Files
memind/src/App.tsx
T
2026-07-25 09:44:26 +08:00

229 lines
6.6 KiB
TypeScript

import { useEffect, useState } from 'react';
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import { checkAuth, logout, setUnauthorizedHandler } from './api/client';
import { clearAllStoredSessionIds } from './utils/sessionStorage';
import { loadBlockedWords } from './utils/wordFilter';
import { AuthView } from './components/AuthView';
import { ChatView } from './components/ChatView';
import { MindSpaceView } from './components/MindSpaceView';
import { ChatProvider } from './context/ChatProvider';
import { PREVIEW_USER } from './dev/mindspacePreviewData';
import { MindSpaceRoute } from './routes/MindSpaceRoute';
import { FeedbackRoutes } from './routes/FeedbackRoute';
import { useProductAnalytics } from './analytics/productAnalytics';
import type { CapabilityMap, PortalUser } from './types';
function isMindSpacePreview() {
return (
import.meta.env.DEV &&
new URLSearchParams(window.location.search).get('preview') === 'mindspace'
);
}
function MindSpaceAuthGate({
user,
onLogout,
}: {
user: PortalUser | null;
onLogout: () => void;
}) {
const location = useLocation();
if (!user) {
const params = new URLSearchParams();
params.set('return_to', `${location.pathname}${location.search}`);
const utmSource = new URLSearchParams(location.search).get('utm_source');
if (utmSource) params.set('utm_source', utmSource);
return <Navigate to={`/?${params.toString()}`} replace />;
}
return <MindSpaceRoute user={user} onLogout={onLogout} />;
}
function AuthenticatedApp({
user,
capabilities,
grantedSkills,
onUserUpdate,
onLogout,
}: {
user: PortalUser | null;
capabilities?: CapabilityMap;
grantedSkills?: string[];
onUserUpdate: (user: PortalUser) => void;
onLogout: () => void;
}) {
const navigate = useNavigate();
const handleLogout = () => {
clearAllStoredSessionIds();
void logout().finally(() => {
onLogout();
navigate('/', { replace: true });
});
};
const chatElement = (
<ChatView
user={user}
capabilities={capabilities}
grantedSkills={grantedSkills}
onUserUpdate={onUserUpdate}
onOpenSpace={
user
? (target) => {
if (target?.pageId) {
navigate(`/space/page/${target.pageId}`);
return;
}
if (target?.categoryCode) {
navigate(`/space?category=${target.categoryCode}`);
return;
}
navigate('/space');
}
: undefined
}
onOpenPage={user ? (pageId) => navigate(`/space/page/${pageId}`) : undefined}
onOpenAdmin={
user?.role === 'admin'
? () => { window.location.href = import.meta.env.VITE_ADMIN_APP_URL ?? '/admin-app'; }
: undefined
}
onOpenFeedback={() => navigate('/feedback')}
onLogout={handleLogout}
/>
);
return (
<ChatProvider
user={user}
capabilities={capabilities}
grantedSkills={grantedSkills}
onUserUpdate={onUserUpdate}
>
<Routes>
<Route
path="/space/*"
element={<MindSpaceAuthGate user={user} onLogout={handleLogout} />}
/>
<Route
path="/feedback/*"
element={<FeedbackRoutes user={user} onLogout={handleLogout} />}
/>
<Route path="/" element={chatElement} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</ChatProvider>
);
}
export function App() {
const mindSpacePreview = isMindSpacePreview();
const navigate = useNavigate();
const [authed, setAuthed] = useState<boolean | null>(null);
const [user, setUser] = useState<PortalUser | null>(null);
const [capabilities, setCapabilities] = useState<CapabilityMap | undefined>();
const [grantedSkills, setGrantedSkills] = useState<string[] | undefined>();
const [legacyMode, setLegacyMode] = useState(false);
const [authUnavailable, setAuthUnavailable] = useState<string | null>(null);
useProductAnalytics(user?.id);
useEffect(() => {
if (mindSpacePreview) return;
setUnauthorizedHandler(() => {
setAuthed(false);
setUser(null);
if (window.location.pathname !== '/') {
navigate('/', { replace: true });
}
});
void checkAuth().then((status) => {
if (status.mode === 'unavailable') {
setAuthUnavailable(status.message ?? '用户认证服务暂时不可用,请稍后刷新');
setAuthed(false);
return;
}
setAuthUnavailable(null);
setLegacyMode(status.mode === 'legacy');
setAuthed(status.authenticated);
setUser(status.user ?? null);
setCapabilities(status.capabilities);
setGrantedSkills(status.grantedSkills);
if (status.authenticated) void loadBlockedWords();
});
return () => setUnauthorizedHandler(null);
}, [mindSpacePreview, navigate]);
if (mindSpacePreview) {
return (
<ChatProvider user={PREVIEW_USER}>
<MindSpaceView
previewMode
user={PREVIEW_USER}
onBack={() => {
window.location.href = '/';
}}
onLogout={() => {
window.location.href = '/';
}}
/>
</ChatProvider>
);
}
if (authed === null) {
return <div className="app-loading"></div>;
}
if (authUnavailable) {
return (
<div className="app-loading">
{authUnavailable}
<button type="button" className="login-link" onClick={() => window.location.reload()}>
</button>
</div>
);
}
if (!authed) {
return (
<AuthView
legacyMode={legacyMode}
onAuth={(nextUser, nextCapabilities, nextSkills) => {
const params = new URLSearchParams(window.location.search);
const returnTo = params.get('return_to') ?? null;
if (returnTo) {
try {
const url = new URL(returnTo, window.location.origin);
if (url.origin === window.location.origin) {
window.location.href = url.toString();
return;
}
} catch {
// ignore invalid URLs
}
}
setAuthed(true);
setUser(nextUser ?? null);
setCapabilities(nextCapabilities);
setGrantedSkills(nextSkills);
}}
/>
);
}
return (
<AuthenticatedApp
user={user}
capabilities={capabilities}
grantedSkills={grantedSkills}
onUserUpdate={setUser}
onLogout={() => {
clearAllStoredSessionIds();
setAuthed(false);
setUser(null);
}}
/>
);
}