721 lines
24 KiB
JavaScript
721 lines
24 KiB
JavaScript
import React, { useState, useEffect, useMemo } from 'react';
|
||
import {
|
||
Routes,
|
||
Route,
|
||
Navigate,
|
||
useNavigate,
|
||
useLocation,
|
||
Link,
|
||
} from 'react-router-dom';
|
||
import { Button } from './components/ui/button.js';
|
||
import { cn } from './utils/cn.js';
|
||
import PromptModal from './components/ui/PromptModal.js';
|
||
import PremiumRoute from './components/PremiumRoute.js';
|
||
import SessionExpiredHandler from './components/SessionExpiredHandler.js';
|
||
import SignInLanding from './components/SignInLanding.js';
|
||
import SignIn from './components/SignIn.js';
|
||
import SignUp from './components/SignUp.js';
|
||
import PlanningLanding from './components/PlanningLanding.js';
|
||
import CareerExplorer from './components/CareerExplorer.js';
|
||
import PreparingLanding from './components/PreparingLanding.js';
|
||
import EducationalProgramsPage from './components/EducationalProgramsPage.js';
|
||
import EnhancingLanding from './components/EnhancingLanding.js';
|
||
import RetirementLanding from './components/RetirementLanding.js';
|
||
import InterestInventory from './components/InterestInventory.js';
|
||
import UserProfile from './components/UserProfile.js';
|
||
import FinancialProfileForm from './components/FinancialProfileForm.js';
|
||
import CareerProfileList from './components/CareerProfileList.js';
|
||
import CareerProfileForm from './components/CareerProfileForm.js';
|
||
import CollegeProfileList from './components/CollegeProfileList.js';
|
||
import CollegeProfileForm from './components/CollegeProfileForm.js';
|
||
import CareerRoadmap from './components/CareerRoadmap.js';
|
||
import Paywall from './components/Paywall.js';
|
||
import OnboardingContainer from './components/PremiumOnboarding/OnboardingContainer.js';
|
||
import RetirementPlanner from './components/RetirementPlanner.js';
|
||
import ResumeRewrite from './components/ResumeRewrite.js';
|
||
import LoanRepaymentPage from './components/LoanRepaymentPage.js';
|
||
import usePageContext from './utils/usePageContext.js';
|
||
import ChatDrawer from './components/ChatDrawer.js';
|
||
import ChatCtx from './contexts/ChatCtx.js';
|
||
import BillingResult from './components/BillingResult.js';
|
||
import SupportModal from './components/SupportModal.js';
|
||
import ForgotPassword from './components/ForgotPassword.js';
|
||
import ResetPassword from './components/ResetPassword.js';
|
||
import { clearToken } from '../auth/authMemory.js';
|
||
|
||
|
||
|
||
|
||
export const ProfileCtx = React.createContext();
|
||
|
||
function ResetPasswordGate() {
|
||
const location = useLocation();
|
||
useEffect(() => {
|
||
try {
|
||
localStorage.removeItem('token');
|
||
localStorage.removeItem('id');
|
||
// If you cache other auth-ish flags, clear them here too
|
||
} catch {}
|
||
// no navigate here; we want to render the reset UI
|
||
}, [location.pathname]);
|
||
|
||
return <ResetPassword />;
|
||
}
|
||
|
||
|
||
function App() {
|
||
const navigate = useNavigate();
|
||
const location = useLocation();
|
||
const { pageContext, snapshot: routeSnapshot } = usePageContext();
|
||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||
const [drawerPane, setDrawerPane] = useState('support');
|
||
const [retireProps, setRetireProps] = useState(null);
|
||
const [supportOpen, setSupportOpen] = useState(false);
|
||
const [userEmail, setUserEmail] = useState('');
|
||
|
||
|
||
const AUTH_HOME = '/signin-landing';
|
||
|
||
/* ------------------------------------------
|
||
ChatDrawer – route-aware tool handlers
|
||
------------------------------------------ */
|
||
const uiToolHandlers = useMemo(() => {
|
||
if (pageContext === "CareerExplorer") {
|
||
return {
|
||
// __tool:addCareerToComparison:{"socCode":"15-2051","careerName":"Data Scientist"}
|
||
addCareerToComparison: ({ socCode, careerName }) => {
|
||
console.log('[dispatch]', socCode, careerName);
|
||
window.dispatchEvent(
|
||
new CustomEvent("add-career", { detail: { socCode, careerName } })
|
||
);
|
||
},
|
||
|
||
// __tool:openCareerModal:{"socCode":"15-2051"}
|
||
openCareerModal: ({ socCode }) => {
|
||
window.dispatchEvent(
|
||
new CustomEvent("open-career", { detail: { socCode } })
|
||
);
|
||
}
|
||
};
|
||
}
|
||
return {}; // every other page exposes no UI tools
|
||
}, [pageContext]);
|
||
|
||
// Retirement bot is only relevant on these pages
|
||
const canShowRetireBot =
|
||
pageContext === 'RetirementPlanner' ||
|
||
pageContext === 'RetirementLanding';
|
||
|
||
// Auth states
|
||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||
const [user, setUser] = useState(null);
|
||
const [chatSnapshot, setChatSnapshot] = useState(null);
|
||
|
||
// Loading state while verifying token
|
||
const [isLoading, setIsLoading] = useState(true);
|
||
|
||
// User states
|
||
const [financialProfile, setFinancialProfile] = useState(null);
|
||
const [scenario, setScenario] = useState(null);
|
||
|
||
// Logout warning modal
|
||
const [showLogoutWarning, setShowLogoutWarning] = useState(false);
|
||
|
||
|
||
// Check if user can access premium
|
||
const canAccessPremium = user?.is_premium || user?.is_pro_premium;
|
||
|
||
const isAuthScreen = React.useMemo(() => {
|
||
const p = location.pathname;
|
||
return (
|
||
p === '/signin' ||
|
||
p === '/signup' ||
|
||
p === '/forgot-password' ||
|
||
p.startsWith('/reset-password')
|
||
);
|
||
}, [location.pathname]);
|
||
|
||
const showAuthedNav = isAuthenticated && !isAuthScreen;
|
||
|
||
// List of premium paths for your CTA logic
|
||
const premiumPaths = [
|
||
'/career-roadmap',
|
||
'/paywall',
|
||
'/financial-profile',
|
||
'/retirement-planner',
|
||
'/premium-onboarding',
|
||
'/enhancing',
|
||
'/retirement',
|
||
'/resume-optimizer',
|
||
];
|
||
|
||
const showPremiumCTA = !premiumPaths.some(p =>
|
||
location.pathname.startsWith(p)
|
||
);
|
||
|
||
|
||
// Helper to see if user is mid–premium-onboarding
|
||
function isOnboardingInProgress() {
|
||
try {
|
||
const stored = JSON.parse(localStorage.getItem('premiumOnboardingState') || '{}');
|
||
// If step < 4 (example), user is in progress
|
||
return stored.step && stored.step < 4;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/* =====================
|
||
Support Modal Email
|
||
===================== */
|
||
useEffect(() => {
|
||
setUserEmail(user?.email || '');
|
||
}, [user]);
|
||
|
||
// ==============================
|
||
// 1) Single Rehydrate UseEffect
|
||
// ==============================
|
||
useEffect(() => {
|
||
// 🚫 Never hydrate auth while on the reset page
|
||
if (location.pathname.startsWith('/reset-password')) {
|
||
try {
|
||
localStorage.removeItem('token');
|
||
localStorage.removeItem('id');
|
||
} catch {}
|
||
setIsAuthenticated(false);
|
||
setUser(null);
|
||
setIsLoading(false);
|
||
return;
|
||
}
|
||
|
||
const token = localStorage.getItem('token');
|
||
|
||
|
||
if (!token) {
|
||
// No token => not authenticated
|
||
setIsLoading(false);
|
||
return;
|
||
}
|
||
|
||
// If we have a token, validate it by fetching user
|
||
fetch('/api/user-profile', {
|
||
headers: { Authorization: `Bearer ${token}` },
|
||
})
|
||
.then((res) => {
|
||
if (!res.ok) throw new Error('Token invalid on server side');
|
||
return res.json();
|
||
})
|
||
.then((profile) => {
|
||
// Successfully got user profile => user is authenticated
|
||
setUser(profile);
|
||
setIsAuthenticated(true);
|
||
})
|
||
.catch((err) => {
|
||
console.error(err);
|
||
// Invalid token => remove it, force sign in
|
||
localStorage.removeItem('token');
|
||
setIsAuthenticated(false);
|
||
setUser(null);
|
||
navigate('/signin?session=expired');
|
||
})
|
||
.finally(() => {
|
||
// Either success or fail, we're done loading
|
||
setIsLoading(false);
|
||
});
|
||
}, [navigate, location.pathname]);
|
||
|
||
// ==========================
|
||
// 2) Logout Handler + Modal
|
||
// ==========================
|
||
const handleLogoutClick = () => {
|
||
if (isOnboardingInProgress()) {
|
||
// Show a modal to confirm losing onboarding data
|
||
setShowLogoutWarning(true);
|
||
} else {
|
||
// No onboarding => just logout
|
||
confirmLogout();
|
||
}
|
||
};
|
||
|
||
|
||
|
||
const confirmLogout = () => {
|
||
localStorage.removeItem('token');
|
||
localStorage.removeItem('id');
|
||
localStorage.removeItem('careerSuggestionsCache');
|
||
localStorage.removeItem('lastSelectedCareerProfileId');
|
||
localStorage.removeItem('selectedCareer');
|
||
localStorage.removeItem('aiClickCount');
|
||
localStorage.removeItem('aiClickDate');
|
||
localStorage.removeItem('aiRecommendations');
|
||
localStorage.removeItem('premiumOnboardingState'); // ← NEW
|
||
localStorage.removeItem('financialProfile'); // ← if you cache it
|
||
|
||
setFinancialProfile(null); // ← reset any React-context copy
|
||
setScenario(null);
|
||
setIsAuthenticated(false);
|
||
setUser(null);
|
||
setShowLogoutWarning(false);
|
||
|
||
// Reset auth
|
||
setIsAuthenticated(false);
|
||
setUser(null);
|
||
setShowLogoutWarning(false);
|
||
|
||
navigate('/signin');
|
||
};
|
||
|
||
|
||
const cancelLogout = () => {
|
||
setShowLogoutWarning(false);
|
||
};
|
||
|
||
// ====================================
|
||
// 3) If still verifying the token, show loading
|
||
// ====================================
|
||
if (isLoading) {
|
||
return (
|
||
<div className="flex h-screen items-center justify-center">
|
||
<p>Loading...</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
|
||
|
||
// =====================
|
||
// Main Render / Layout
|
||
// =====================
|
||
return (
|
||
<ProfileCtx.Provider
|
||
value={{ financialProfile, setFinancialProfile,
|
||
scenario, setScenario,
|
||
user, setUser}}
|
||
>
|
||
<ChatCtx.Provider value={{ setChatSnapshot,
|
||
openSupport: () => { setDrawerPane('support'); setDrawerOpen(true); },
|
||
openRetire : (props) => {
|
||
if (!canShowRetireBot) {
|
||
console.warn('Retirement bot disabled on this page');
|
||
return;
|
||
}
|
||
|
||
setRetireProps(props);
|
||
setDrawerPane('retire');
|
||
setDrawerOpen(true);
|
||
}
|
||
}}>
|
||
<div className="flex min-h-screen flex-col bg-gray-50 text-gray-800">
|
||
{/* Header */}
|
||
<header className="flex items-center justify-between border-b bg-white px-6 py-4 shadow-sm relative">
|
||
<h1 className="text-lg font-semibold">
|
||
AptivaAI - Career Guidance Platform
|
||
</h1>
|
||
|
||
{showAuthedNav && (
|
||
<>
|
||
{/* NAV MENU */}
|
||
<nav className="flex space-x-6">
|
||
{/* 1) Planning */}
|
||
<div className="relative group">
|
||
<Button
|
||
style={{ color: '#1f2937' }}
|
||
className={cn(
|
||
'bg-white',
|
||
'border border-gray-300',
|
||
'hover:bg-gray-100',
|
||
'hover:text-blue-700',
|
||
'whitespace-nowrap',
|
||
'text-xs sm:text-sm md:text-base',
|
||
'font-semibold'
|
||
)}
|
||
onClick={() => navigate('/planning')}
|
||
>
|
||
Find Your Career
|
||
</Button>
|
||
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-48 z-50">
|
||
<Link
|
||
to="/career-explorer"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
Career Explorer
|
||
</Link>
|
||
<Link
|
||
to="/interest-inventory"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
Interest Inventory
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 2) Preparing */}
|
||
<div className="relative group">
|
||
<Button
|
||
style={{ color: '#1f2937' }}
|
||
className={cn(
|
||
'bg-white',
|
||
'border border-gray-300',
|
||
'hover:bg-gray-100',
|
||
'hover:text-blue-700',
|
||
'whitespace-nowrap',
|
||
'text-xs sm:text-sm md:text-base',
|
||
'font-semibold'
|
||
)}
|
||
onClick={() => navigate('/preparing')}
|
||
>
|
||
Preparing & UpSkilling for Your Career
|
||
</Button>
|
||
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-56 z-50">
|
||
<Link
|
||
to="/educational-programs"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
Educational Programs
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 3) Enhancing (Premium) */}
|
||
<div className="relative group">
|
||
<Button
|
||
style={{ color: '#1f2937' }}
|
||
className={cn(
|
||
'bg-white',
|
||
'border border-gray-300',
|
||
'hover:bg-gray-100',
|
||
'hover:text-blue-700',
|
||
'whitespace-nowrap',
|
||
'text-xs sm:text-sm md:text-base',
|
||
'font-semibold'
|
||
)}
|
||
onClick={() => navigate('/enhancing')}
|
||
>
|
||
Enhancing Your Career
|
||
{!canAccessPremium && (
|
||
<span className="text-xs ml-1 text-gray-600">
|
||
(Premium)
|
||
</span>
|
||
)}
|
||
</Button>
|
||
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-56 z-50">
|
||
<Link
|
||
to="/career-roadmap"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
Roadmap & AI Career Coach
|
||
</Link>
|
||
<Link
|
||
to="/resume-optimizer"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
Optimize Resume
|
||
</Link>
|
||
{/* etc. */}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 4) Retirement (Premium) */}
|
||
<div className="relative group">
|
||
<Button
|
||
style={{ color: '#1f2937' }}
|
||
className={cn(
|
||
'bg-white',
|
||
'border border-gray-300',
|
||
'hover:bg-gray-100',
|
||
'hover:text-blue-700',
|
||
'whitespace-nowrap',
|
||
'text-xs sm:text-sm md:text-base',
|
||
'font-semibold'
|
||
)}
|
||
onClick={() => navigate('/retirement')}
|
||
>
|
||
Retirement Planning (beta)
|
||
{!canAccessPremium && (
|
||
<span className="text-xs ml-1 text-gray-600">
|
||
(Premium)
|
||
</span>
|
||
)}
|
||
</Button>
|
||
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-56 z-50">
|
||
{/* Additional retirement menu items */}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 5) Profile */}
|
||
<div className="relative group">
|
||
<Button
|
||
style={{ color: '#1f2937' }}
|
||
className={cn(
|
||
'bg-white',
|
||
'border border-gray-300',
|
||
'hover:bg-gray-100',
|
||
'hover:text-blue-700',
|
||
'whitespace-nowrap',
|
||
'text-xs sm:text-sm md:text-base',
|
||
'font-semibold',
|
||
'min-w-0',
|
||
'max-w-[90px]',
|
||
'truncate'
|
||
)}
|
||
>
|
||
Profile
|
||
</Button>
|
||
<div className="absolute top-full left-0 hidden group-hover:block bg-white border shadow-md w-48 z-50">
|
||
<Link
|
||
to="/profile"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
Account
|
||
</Link>
|
||
<Link
|
||
to="/financial-profile"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
Financial Profile
|
||
</Link>
|
||
{canAccessPremium ? (
|
||
/* Premium users go straight to the wizard */
|
||
<Link
|
||
to="/profile/careers"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
Career Profiles
|
||
</Link>
|
||
) : (
|
||
<span
|
||
className="block px-4 py-2 text-sm text-gray-400 cursor-not-allowed"
|
||
>
|
||
Career Profiles (Premium)
|
||
</span>
|
||
)}
|
||
|
||
{/* College Profiles (go straight to list) */}
|
||
{canAccessPremium ? (
|
||
<Link
|
||
to="/profile/college"
|
||
className="block px-4 py-2 hover:bg-gray-100 text-sm text-gray-700"
|
||
>
|
||
College Profiles
|
||
</Link>
|
||
) : (
|
||
<span className="block px-4 py-2 text-sm text-gray-400 cursor-not-allowed">
|
||
College Profiles (Premium)
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
{/* LOGOUT + UPGRADE BUTTONS */}
|
||
<div className="flex items-center space-x-4 ml-4 relative z-10">
|
||
{showPremiumCTA && !canAccessPremium && (
|
||
<Button
|
||
className={cn(
|
||
'bg-green-500 hover:bg-green-600',
|
||
'max-w-fit text-center text-white',
|
||
'px-3 py-2',
|
||
'rounded',
|
||
'whitespace-nowrap',
|
||
'text-sm',
|
||
'font-semibold',
|
||
'shadow'
|
||
)}
|
||
style={{ minWidth: 0, width: 'auto' }}
|
||
onClick={() => navigate('/paywall')}
|
||
>
|
||
Upgrade to Premium
|
||
</Button>
|
||
)}
|
||
|
||
<button
|
||
type="button"
|
||
onClick={() => setSupportOpen(true)}
|
||
className="px-3 py-1 rounded hover:bg-gray-100"
|
||
>
|
||
Support
|
||
</button>
|
||
|
||
<SupportModal
|
||
open={supportOpen}
|
||
onClose={() => setSupportOpen(false)}
|
||
userEmail={userEmail}
|
||
/>
|
||
|
||
{/* LOGOUT BUTTON */}
|
||
<button
|
||
className="text-red-600 hover:text-red-800 bg-transparent border-none"
|
||
onClick={handleLogoutClick}
|
||
>
|
||
Logout
|
||
</button>
|
||
|
||
{/* SHOW WARNING MODAL IF needed */}
|
||
{showLogoutWarning && (
|
||
<PromptModal
|
||
open={true}
|
||
title="End Onboarding?"
|
||
message="If you sign out now, your onboarding progress will be lost. Are you sure?"
|
||
confirmText="Sign Out"
|
||
cancelText="Nevermind"
|
||
onConfirm={confirmLogout}
|
||
onCancel={cancelLogout}
|
||
/>
|
||
)}
|
||
</div>
|
||
</>
|
||
)}
|
||
</header>
|
||
|
||
{/* MAIN CONTENT */}
|
||
<main className="flex-1 p-6">
|
||
<Routes>
|
||
{/* Default */}
|
||
<Route
|
||
path="/"
|
||
element={<Navigate to={isAuthenticated ? AUTH_HOME : '/signin'} replace />}
|
||
/>
|
||
|
||
<Route path="/reset-password/:token" element={<ResetPasswordGate />} />
|
||
|
||
{/* Public (guest-only) routes */}
|
||
<Route
|
||
path="/signin"
|
||
element={
|
||
isAuthenticated ? (
|
||
<Navigate to={AUTH_HOME} replace />
|
||
) : (
|
||
<SignIn
|
||
setIsAuthenticated={setIsAuthenticated}
|
||
setUser={setUser}
|
||
setFinancialProfile={setFinancialProfile}
|
||
setScenario={setScenario}
|
||
/>
|
||
)
|
||
}
|
||
/>
|
||
|
||
<Route
|
||
path="/signup"
|
||
element={isAuthenticated ? <Navigate to={AUTH_HOME} replace /> : <SignUp setUser={setUser} />}
|
||
/>
|
||
|
||
<Route
|
||
path="/forgot-password"
|
||
element={isAuthenticated ? <Navigate to={AUTH_HOME} replace /> : <ForgotPassword />}
|
||
/>
|
||
|
||
|
||
<Route path="/paywall" element={<Paywall />} />
|
||
|
||
{/* Authenticated routes */}
|
||
{isAuthenticated && (
|
||
<>
|
||
<Route path="/signin-landing" element={<SignInLanding user={user} />} />
|
||
<Route path="/interest-inventory" element={<InterestInventory />} />
|
||
<Route path="/profile" element={<UserProfile />} />
|
||
<Route path="/planning" element={<PlanningLanding />} />
|
||
<Route path="/career-explorer" element={<CareerExplorer />} />
|
||
<Route path="/loan-repayment" element={<LoanRepaymentPage />} />
|
||
<Route path="/educational-programs" element={<EducationalProgramsPage />} />
|
||
<Route path="/preparing" element={<PreparingLanding />} />
|
||
<Route path="/billing" element={<BillingResult />} />
|
||
|
||
{/* Premium-wrapped */}
|
||
<Route
|
||
path="/enhancing"
|
||
element={
|
||
<PremiumRoute user={user}>
|
||
<EnhancingLanding />
|
||
</PremiumRoute>
|
||
}
|
||
/>
|
||
<Route
|
||
path="/retirement"
|
||
element={
|
||
<PremiumRoute user={user}>
|
||
<RetirementLanding />
|
||
</PremiumRoute>
|
||
}
|
||
/>
|
||
<Route
|
||
path="/career-roadmap/:careerId?"
|
||
element={
|
||
<PremiumRoute user={user}>
|
||
<CareerRoadmap />
|
||
</PremiumRoute>
|
||
}
|
||
/>
|
||
<Route path="/profile/careers" element={<CareerProfileList />} />
|
||
<Route path="/profile/careers/:id/edit" element={<CareerProfileForm />} />
|
||
<Route path="/profile/college" element={<CollegeProfileList />} />
|
||
<Route path="/profile/college/:careerId/:id?" element={<CollegeProfileForm />} />
|
||
<Route
|
||
path="/financial-profile"
|
||
element={
|
||
<PremiumRoute user={user}>
|
||
<FinancialProfileForm />
|
||
</PremiumRoute>
|
||
}
|
||
/>
|
||
<Route
|
||
path="/retirement-planner"
|
||
element={
|
||
<PremiumRoute user={user}>
|
||
<RetirementPlanner />
|
||
</PremiumRoute>
|
||
}
|
||
/>
|
||
<Route
|
||
path="/premium-onboarding"
|
||
element={
|
||
<PremiumRoute user={user}>
|
||
<OnboardingContainer />
|
||
</PremiumRoute>
|
||
}
|
||
/>
|
||
<Route
|
||
path="/resume-optimizer"
|
||
element={
|
||
<PremiumRoute user={user}>
|
||
<ResumeRewrite />
|
||
</PremiumRoute>
|
||
}
|
||
/>
|
||
</>
|
||
)}
|
||
|
||
|
||
{/* 404 / Fallback */}
|
||
<Route
|
||
path="*"
|
||
element={<Navigate to={isAuthenticated ? AUTH_HOME : '/signin'} replace />}
|
||
/>
|
||
</Routes>
|
||
</main>
|
||
|
||
<ChatDrawer
|
||
open={drawerOpen}
|
||
onOpenChange={setDrawerOpen}
|
||
|
||
pane={drawerPane}
|
||
setPane={setDrawerPane}
|
||
|
||
retireProps={retireProps}
|
||
|
||
pageContext={pageContext}
|
||
snapshot={chatSnapshot}
|
||
uiToolHandlers={uiToolHandlers}
|
||
canShowRetireBot={canShowRetireBot}
|
||
/>
|
||
|
||
{/* Session Handler (Optional) */}
|
||
<SessionExpiredHandler />
|
||
</div>
|
||
</ChatCtx.Provider>
|
||
</ProfileCtx.Provider>
|
||
);
|
||
}
|
||
|
||
export default App;
|