mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2149e9593a | |||
| 543ef2ef38 | |||
| 0aa757118e | |||
| 6c1199c93b | |||
| 580ed74fe7 | |||
| 3e847ddb01 | |||
| 11a11a6d20 | |||
| 387337d1e1 | |||
| 95f81ec3f5 |
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
|
||||
import { DesktopWorkspace, MobileWorkspace } from './components/WorkspaceLayout';
|
||||
import TelemetryNotification from './components/features/TelemetryNotification';
|
||||
@@ -17,6 +17,16 @@ const MobileChatPage = memo(() => {
|
||||
});
|
||||
|
||||
const DesktopChatPage = memo(() => {
|
||||
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('DesktopChatPage');
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!isMounted) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle />
|
||||
|
||||
@@ -24,6 +24,7 @@ const CloudBanner = dynamic(() => import('@/features/AlertBanner/CloudBanner'));
|
||||
|
||||
const Layout = memo((props: { locale: Locales }) => {
|
||||
const { locale } = props;
|
||||
|
||||
const { isPWA } = usePlatform();
|
||||
const theme = useTheme();
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
'use client';
|
||||
|
||||
import { memo, Suspense, useMemo } from 'react';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import type { Locales } from '@/types/locale';
|
||||
|
||||
import { createDesktopRouter } from './desktopRouter.config';
|
||||
|
||||
interface ClientRouterProps {
|
||||
locale: Locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure CSR Loading Fallback Component
|
||||
*
|
||||
* This component is displayed during:
|
||||
* - Initial router data loading
|
||||
* - Route transitions with loaders
|
||||
* - Any async route-level operations
|
||||
*
|
||||
* NOTE: This runs ONLY on the client, never during SSR
|
||||
*/
|
||||
const RouterLoadingFallback = () => (
|
||||
<div
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
justifyContent: 'center',
|
||||
width: '100vw',
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
animation: 'spin 1s linear infinite',
|
||||
border: '4px solid #f3f3f3',
|
||||
borderRadius: '50%',
|
||||
borderTop: '4px solid #3498db',
|
||||
height: '40px',
|
||||
margin: '0 auto 16px',
|
||||
width: '40px',
|
||||
}}
|
||||
/>
|
||||
<p style={{ color: '#666', fontSize: '14px' }}>Loading...</p>
|
||||
</div>
|
||||
<style>{`
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Desktop Client Router Component
|
||||
*
|
||||
* Pure CSR (Client-Side Rendering) implementation:
|
||||
* - Wrapped with Next.js dynamic import (ssr: false) to prevent SSR
|
||||
* - Uses React Suspense for client-side loading states
|
||||
* - Router instance is memoized based on locale
|
||||
* - All route loaders execute ONLY in the browser
|
||||
*
|
||||
* This component uses ReactDOM.createRoot semantics (via Next.js dynamic import),
|
||||
* NOT hydration. There is no server-rendered HTML to match against.
|
||||
*/
|
||||
const DesktopClientRouter = memo<ClientRouterProps>(({ locale }) => {
|
||||
const router = useMemo(() => createDesktopRouter(locale), [locale]);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<RouterLoadingFallback />}>
|
||||
<RouterProvider router={router} />
|
||||
</Suspense>
|
||||
);
|
||||
});
|
||||
|
||||
DesktopClientRouter.displayName = 'DesktopClientRouter';
|
||||
|
||||
export default DesktopClientRouter;
|
||||
@@ -1,38 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import BootErrorBoundary from '@/components/BootErrorBoundary';
|
||||
import Loading from '@/components/Loading/BrandTextLoading';
|
||||
import type { Locales } from '@/types/locale';
|
||||
|
||||
import { createDesktopRouter } from './desktopRouter.config';
|
||||
|
||||
interface ClientRouterProps {
|
||||
locale: Locales;
|
||||
}
|
||||
|
||||
const ClientRouter = memo<ClientRouterProps>(({ locale }) => {
|
||||
const router = useMemo(() => createDesktopRouter(locale), [locale]);
|
||||
return (
|
||||
<BootErrorBoundary fallback={<Loading />}>
|
||||
<RouterProvider router={router} />
|
||||
</BootErrorBoundary>
|
||||
);
|
||||
});
|
||||
|
||||
ClientRouter.displayName = 'ClientRouter';
|
||||
|
||||
const DesktopRouterClient = dynamic(() => Promise.resolve(ClientRouter), {
|
||||
loading: () => <Loading />,
|
||||
/**
|
||||
* Dynamic import with SSR disabled
|
||||
*
|
||||
* This creates a pure CSR boundary:
|
||||
* - Server renders: nothing (null)
|
||||
* - Client hydration: null (matches server)
|
||||
* - After hydration: DesktopClientRouter mounts with ReactDOM.createRoot semantics
|
||||
*
|
||||
* The loading component is optional here since DesktopClientRouter
|
||||
* has its own fallbackElement for route-level loading states.
|
||||
*/
|
||||
const DesktopRouterClient = dynamic(() => import('./DesktopClientRouter'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
interface DesktopRouterProps {
|
||||
locale: Locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* Desktop Router Wrapper
|
||||
*
|
||||
* This wrapper exists to:
|
||||
* 1. Create a client component boundary for Next.js
|
||||
* 2. Enable code splitting (Desktop bundle separated from Mobile)
|
||||
* 3. Ensure the entire react-router-dom tree is client-only
|
||||
*/
|
||||
const DesktopRouter = ({ locale }: DesktopRouterProps) => {
|
||||
return <DesktopRouterClient locale={locale} />;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
'use client';
|
||||
|
||||
import { memo, Suspense, useMemo } from 'react';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import type { Locales } from '@/types/locale';
|
||||
|
||||
import { createMobileRouter } from './mobileRouter.config';
|
||||
|
||||
interface ClientRouterProps {
|
||||
locale: Locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure CSR Loading Fallback Component
|
||||
*
|
||||
* This component is displayed during:
|
||||
* - Initial router data loading
|
||||
* - Route transitions with loaders
|
||||
* - Any async route-level operations
|
||||
*
|
||||
* NOTE: This runs ONLY on the client, never during SSR
|
||||
*/
|
||||
const RouterLoadingFallback = () => (
|
||||
<div
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
height: '100vh',
|
||||
justifyContent: 'center',
|
||||
width: '100vw',
|
||||
}}
|
||||
>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
animation: 'spin 1s linear infinite',
|
||||
border: '4px solid #f3f3f3',
|
||||
borderRadius: '50%',
|
||||
borderTop: '4px solid #3498db',
|
||||
height: '40px',
|
||||
margin: '0 auto 16px',
|
||||
width: '40px',
|
||||
}}
|
||||
/>
|
||||
<p style={{ color: '#666', fontSize: '14px' }}>Loading...</p>
|
||||
</div>
|
||||
<style>{`
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Mobile Client Router Component
|
||||
*
|
||||
* Pure CSR (Client-Side Rendering) implementation:
|
||||
* - Wrapped with Next.js dynamic import (ssr: false) to prevent SSR
|
||||
* - Uses React Suspense for client-side loading states
|
||||
* - Router instance is memoized based on locale
|
||||
* - All route loaders execute ONLY in the browser
|
||||
*
|
||||
* This component uses ReactDOM.createRoot semantics (via Next.js dynamic import),
|
||||
* NOT hydration. There is no server-rendered HTML to match against.
|
||||
*/
|
||||
const MobileClientRouter = memo<ClientRouterProps>(({ locale }) => {
|
||||
const router = useMemo(() => createMobileRouter(locale), [locale]);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<RouterLoadingFallback />}>
|
||||
<RouterProvider router={router} />
|
||||
</Suspense>
|
||||
);
|
||||
});
|
||||
|
||||
MobileClientRouter.displayName = 'MobileClientRouter';
|
||||
|
||||
export default MobileClientRouter;
|
||||
@@ -1,39 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
|
||||
import BootErrorBoundary from '@/components/BootErrorBoundary';
|
||||
import Loading from '@/components/Loading/BrandTextLoading';
|
||||
import type { Locales } from '@/types/locale';
|
||||
|
||||
import { createMobileRouter } from './mobileRouter.config';
|
||||
|
||||
interface ClientRouterProps {
|
||||
locale: Locales;
|
||||
}
|
||||
|
||||
const ClientRouter = memo<ClientRouterProps>(({ locale }) => {
|
||||
const router = useMemo(() => createMobileRouter(locale), [locale]);
|
||||
|
||||
return (
|
||||
<BootErrorBoundary fallback={<Loading />}>
|
||||
<RouterProvider router={router} />
|
||||
</BootErrorBoundary>
|
||||
);
|
||||
});
|
||||
|
||||
ClientRouter.displayName = 'ClientRouter';
|
||||
|
||||
const MobileRouterClient = dynamic(() => Promise.resolve(ClientRouter), {
|
||||
loading: () => <Loading />,
|
||||
/**
|
||||
* Dynamic import with SSR disabled
|
||||
*
|
||||
* This creates a pure CSR boundary:
|
||||
* - Server renders: nothing (null)
|
||||
* - Client hydration: null (matches server)
|
||||
* - After hydration: MobileClientRouter mounts with ReactDOM.createRoot semantics
|
||||
*
|
||||
* The loading component is optional here since MobileClientRouter
|
||||
* has its own fallbackElement for route-level loading states.
|
||||
*/
|
||||
const MobileRouterClient = dynamic(() => import('./MobileClientRouter'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
interface MobileRouterProps {
|
||||
locale: Locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mobile Router Wrapper
|
||||
*
|
||||
* This wrapper exists to:
|
||||
* 1. Create a client component boundary for Next.js
|
||||
* 2. Enable code splitting (Mobile bundle separated from Desktop)
|
||||
* 3. Ensure the entire react-router-dom tree is client-only
|
||||
*/
|
||||
const MobileRouter = ({ locale }: MobileRouterProps) => {
|
||||
return <MobileRouterClient locale={locale} />;
|
||||
};
|
||||
|
||||
@@ -1,74 +1,172 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { type LoaderFunction, createBrowserRouter, redirect, useNavigate } from 'react-router-dom';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { createBrowserRouter, redirect } from 'react-router-dom';
|
||||
|
||||
import Loading from '@/components/Loading/BrandTextLoading';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import type { Locales } from '@/types/locale';
|
||||
|
||||
import DesktopMainLayout from './(main)/layouts/desktop';
|
||||
import { idLoader, slugLoader } from './loaders/routeParams';
|
||||
|
||||
// Component to register navigate function in global store
|
||||
const NavigatorRegistrar = () => {
|
||||
const navigate = useNavigate();
|
||||
/**
|
||||
* Desktop Router Configuration - Pure CSR Mode
|
||||
*
|
||||
* IMPORTANT: This router runs ONLY in the browser (client-side).
|
||||
*
|
||||
* Key characteristics:
|
||||
* - createBrowserRouter uses window.history API (client-only)
|
||||
* - All loaders execute in the browser during navigation
|
||||
* - No server-side rendering or hydration involved
|
||||
* - Route data fetching happens on-demand during client navigation
|
||||
*
|
||||
* The entire router tree is wrapped with Next.js dynamic import (ssr: false),
|
||||
* ensuring this code never executes on the server.
|
||||
*/
|
||||
|
||||
useEffect(() => {
|
||||
useGlobalStore.setState({ navigate });
|
||||
// Chat components
|
||||
const DesktopChatPage = dynamic(
|
||||
() => import('./(main)/chat/index').then((m) => m.DesktopChatPage),
|
||||
{ ssr: false },
|
||||
);
|
||||
const ChatLayout = dynamic(() => import('./(main)/chat/_layout/Desktop'), { ssr: false });
|
||||
|
||||
return () => {
|
||||
useGlobalStore.setState({ navigate: undefined });
|
||||
};
|
||||
}, [navigate]);
|
||||
// Discover List components
|
||||
const DesktopHomePage = dynamic(
|
||||
() => import('./(main)/discover/(list)/(home)/index').then((m) => m.DesktopHomePage),
|
||||
{ ssr: false },
|
||||
);
|
||||
const DesktopAssistantPage = dynamic(
|
||||
() => import('./(main)/discover/(list)/assistant/index').then((m) => m.DesktopAssistantPage),
|
||||
{ ssr: false },
|
||||
);
|
||||
const DiscoverAssistantLayout = dynamic(
|
||||
() => import('./(main)/discover/(list)/assistant/_layout/Desktop'),
|
||||
{ ssr: false },
|
||||
);
|
||||
const DiscoverListMcpPage = dynamic(
|
||||
() => import('./(main)/discover/(list)/mcp/index').then((m) => m.DesktopMcpPage),
|
||||
{ ssr: false },
|
||||
);
|
||||
const DiscoverMcpLayout = dynamic(
|
||||
() => import('./(main)/discover/(list)/mcp/_layout/Desktop'),
|
||||
{ ssr: false },
|
||||
);
|
||||
const DiscoverListModelPage = dynamic(
|
||||
() => import('./(main)/discover/(list)/model/index').then((m) => m.DesktopModelPage),
|
||||
{ ssr: false },
|
||||
);
|
||||
const DiscoverModelLayout = dynamic(
|
||||
() => import('./(main)/discover/(list)/model/_layout/Desktop'),
|
||||
{ ssr: false },
|
||||
);
|
||||
const DiscoverListProviderPage = dynamic(
|
||||
() => import('./(main)/discover/(list)/provider/index').then((m) => m.DesktopProviderPage),
|
||||
{ ssr: false },
|
||||
);
|
||||
const DiscoverListLayout = dynamic(
|
||||
() => import('./(main)/discover/(list)/_layout/Desktop/index'),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
return null;
|
||||
};
|
||||
// Discover Detail components
|
||||
const DesktopDiscoverAssistantDetailPage = dynamic(
|
||||
() =>
|
||||
import('./(main)/discover/(detail)/assistant/index').then(
|
||||
(m) => m.DesktopDiscoverAssistantDetailPage,
|
||||
),
|
||||
{ ssr: false },
|
||||
);
|
||||
const DiscoverDetailMcpPage = dynamic(
|
||||
() => import('./(main)/discover/(detail)/mcp/index').then((m) => m.DesktopMcpPage),
|
||||
{ ssr: false },
|
||||
);
|
||||
const DiscoverDetailModelPage = dynamic(
|
||||
() => import('./(main)/discover/(detail)/model/index').then((m) => m.DesktopModelPage),
|
||||
{ ssr: false },
|
||||
);
|
||||
const DiscoverDetailProviderPage = dynamic(
|
||||
() => import('./(main)/discover/(detail)/provider/index').then((m) => m.DesktopProviderPage),
|
||||
{ ssr: false },
|
||||
);
|
||||
const DiscoverDetailLayout = dynamic(
|
||||
() => import('./(main)/discover/(detail)/_layout/Desktop'),
|
||||
{ ssr: false },
|
||||
);
|
||||
const DiscoverLayout = dynamic(
|
||||
() => import('./(main)/discover/_layout/Desktop/index'),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
// Root layout wrapper component - just registers navigator and renders outlet
|
||||
// Note: Desktop layout is provided by individual route components
|
||||
const RootLayout = (props: { locale: Locales }) => {
|
||||
return (
|
||||
<>
|
||||
<NavigatorRegistrar />
|
||||
<DesktopMainLayout locale={props.locale} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
// Knowledge components
|
||||
const KnowledgeHome = dynamic(() => import('./(main)/knowledge/routes/KnowledgeHome'), {
|
||||
ssr: false,
|
||||
});
|
||||
const KnowledgeBasesList = dynamic(() => import('./(main)/knowledge/routes/KnowledgeBasesList'), {
|
||||
ssr: false,
|
||||
});
|
||||
const KnowledgeBaseDetail = dynamic(
|
||||
() => import('./(main)/knowledge/routes/KnowledgeBaseDetail'),
|
||||
{ ssr: false },
|
||||
);
|
||||
const KnowledgeLayout = dynamic(() => import('./(main)/knowledge/_layout/Desktop'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
// Hydration gate loader -always return true to bypass hydration gate
|
||||
const hydrationGateLoader: LoaderFunction = () => {
|
||||
return true
|
||||
};
|
||||
// Settings components
|
||||
const SettingsLayout = dynamic(() => import('./(main)/settings/_layout/Desktop'), { ssr: false });
|
||||
const SettingsLayoutWrapper = dynamic(() => import('./(main)/settings/_layout/DesktopWrapper'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
// Image components
|
||||
const ImagePage = dynamic(() => import('./(main)/image'), { ssr: false });
|
||||
const ImageLayoutWrapper = dynamic(() => import('./(main)/image/_layout/DesktopWrapper'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
// Labs components
|
||||
const LabsPage = dynamic(() => import('./(main)/labs'), { ssr: false });
|
||||
|
||||
// Profile components
|
||||
const ProfileHomePage = dynamic(() => import('./(main)/profile/(home)/desktop'), { ssr: false });
|
||||
const ProfileApikeyPage = dynamic(() => import('./(main)/profile/apikey/index'), { ssr: false });
|
||||
const DesktopProfileSecurityPage = dynamic(
|
||||
() => import('./(main)/profile/security/index').then((m) => m.DesktopProfileSecurityPage),
|
||||
{ ssr: false },
|
||||
);
|
||||
const DesktopProfileStatsPage = dynamic(
|
||||
() => import('./(main)/profile/stats/index').then((m) => m.DesktopProfileStatsPage),
|
||||
{ ssr: false },
|
||||
);
|
||||
const DesktopProfileUsagePage = dynamic(
|
||||
() => import('./(main)/profile/usage/index').then((m) => m.DesktopProfileUsagePage),
|
||||
{ ssr: false },
|
||||
);
|
||||
const ProfileLayoutWrapper = dynamic(() => import('./(main)/profile/_layout/DesktopWrapper'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
// Root layout wrapper component
|
||||
const RootLayout = (props: { locale: Locales }) => <DesktopMainLayout locale={props.locale} />;
|
||||
|
||||
// Create desktop router configuration
|
||||
export const createDesktopRouter = (locale: Locales) =>
|
||||
createBrowserRouter([
|
||||
{
|
||||
HydrateFallback: () => <Loading />,
|
||||
children: [
|
||||
// Chat routes
|
||||
{
|
||||
children: [
|
||||
{
|
||||
element: <DesktopChatPage />,
|
||||
index: true,
|
||||
lazy: () =>
|
||||
import('./(main)/chat/index').then((m) => ({
|
||||
Component: m.DesktopChatPage,
|
||||
})),
|
||||
},
|
||||
{
|
||||
lazy: () =>
|
||||
import('./(main)/chat/index').then((m) => ({
|
||||
Component: m.DesktopChatPage,
|
||||
})),
|
||||
element: <DesktopChatPage />,
|
||||
path: '*',
|
||||
},
|
||||
],
|
||||
lazy: () =>
|
||||
import('./(main)/chat/_layout/Desktop').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
element: <ChatLayout />,
|
||||
path: 'chat',
|
||||
},
|
||||
|
||||
@@ -81,117 +179,72 @@ export const createDesktopRouter = (locale: Locales) =>
|
||||
{
|
||||
children: [
|
||||
{
|
||||
element: <DesktopAssistantPage />,
|
||||
index: true,
|
||||
lazy: () =>
|
||||
import('./(main)/discover/(list)/assistant/index').then((m) => ({
|
||||
Component: m.DesktopAssistantPage,
|
||||
})),
|
||||
},
|
||||
],
|
||||
lazy: () =>
|
||||
import('./(main)/discover/(list)/assistant/_layout/Desktop').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
element: <DiscoverAssistantLayout />,
|
||||
path: 'assistant',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
element: <DiscoverListModelPage />,
|
||||
index: true,
|
||||
lazy: () =>
|
||||
import('./(main)/discover/(list)/model/index').then((m) => ({
|
||||
Component: m.DesktopModelPage,
|
||||
})),
|
||||
},
|
||||
],
|
||||
lazy: () =>
|
||||
import('./(main)/discover/(list)/model/_layout/Desktop').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
element: <DiscoverModelLayout />,
|
||||
path: 'model',
|
||||
},
|
||||
{
|
||||
lazy: () =>
|
||||
import('./(main)/discover/(list)/provider/index').then((m) => ({
|
||||
Component: m.DesktopProviderPage,
|
||||
})),
|
||||
element: <DiscoverListProviderPage />,
|
||||
path: 'provider',
|
||||
},
|
||||
{
|
||||
children: [
|
||||
{
|
||||
element: <DiscoverListMcpPage />,
|
||||
index: true,
|
||||
lazy: () =>
|
||||
import('./(main)/discover/(list)/mcp/index').then((m) => ({
|
||||
Component: m.DesktopMcpPage,
|
||||
})),
|
||||
},
|
||||
],
|
||||
lazy: () =>
|
||||
import('./(main)/discover/(list)/mcp/_layout/Desktop').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
element: <DiscoverMcpLayout />,
|
||||
path: 'mcp',
|
||||
},
|
||||
{
|
||||
element: <DesktopHomePage />,
|
||||
index: true,
|
||||
lazy: () =>
|
||||
import('./(main)/discover/(list)/(home)/index').then((m) => ({
|
||||
Component: m.DesktopHomePage,
|
||||
})),
|
||||
},
|
||||
],
|
||||
lazy: () =>
|
||||
import('./(main)/discover/(list)/_layout/Desktop/index').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
element: <DiscoverListLayout />,
|
||||
},
|
||||
// Detail routes (with DetailLayout)
|
||||
{
|
||||
children: [
|
||||
{
|
||||
lazy: () =>
|
||||
import('./(main)/discover/(detail)/assistant/index').then((m) => ({
|
||||
Component: m.DesktopDiscoverAssistantDetailPage,
|
||||
})),
|
||||
element: <DesktopDiscoverAssistantDetailPage />,
|
||||
loader: slugLoader,
|
||||
path: 'assistant/:slug',
|
||||
},
|
||||
{
|
||||
lazy: () =>
|
||||
import('./(main)/discover/(detail)/model/index').then((m) => ({
|
||||
Component: m.DesktopModelPage,
|
||||
})),
|
||||
element: <DiscoverDetailModelPage />,
|
||||
loader: slugLoader,
|
||||
path: 'model/:slug',
|
||||
},
|
||||
{
|
||||
lazy: () =>
|
||||
import('./(main)/discover/(detail)/provider/index').then((m) => ({
|
||||
Component: m.DesktopProviderPage,
|
||||
})),
|
||||
element: <DiscoverDetailProviderPage />,
|
||||
loader: slugLoader,
|
||||
path: 'provider/:slug',
|
||||
},
|
||||
{
|
||||
lazy: () =>
|
||||
import('./(main)/discover/(detail)/mcp/index').then((m) => ({
|
||||
Component: m.DesktopMcpPage,
|
||||
})),
|
||||
element: <DiscoverDetailMcpPage />,
|
||||
loader: slugLoader,
|
||||
path: 'mcp/:slug',
|
||||
},
|
||||
],
|
||||
lazy: () =>
|
||||
import('./(main)/discover/(detail)/_layout/Desktop').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
element: <DiscoverDetailLayout />,
|
||||
},
|
||||
],
|
||||
lazy: () =>
|
||||
import('./(main)/discover/_layout/Desktop/index').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
element: <DiscoverLayout />,
|
||||
path: 'discover',
|
||||
},
|
||||
|
||||
@@ -199,40 +252,25 @@ export const createDesktopRouter = (locale: Locales) =>
|
||||
{
|
||||
children: [
|
||||
{
|
||||
element: <KnowledgeHome />,
|
||||
index: true,
|
||||
lazy: () =>
|
||||
import('./(main)/knowledge/routes/KnowledgeHome').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
},
|
||||
{
|
||||
lazy: () =>
|
||||
import('./(main)/knowledge/routes/KnowledgeBasesList').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
element: <KnowledgeBasesList />,
|
||||
path: 'bases',
|
||||
},
|
||||
{
|
||||
lazy: () =>
|
||||
import('./(main)/knowledge/routes/KnowledgeBaseDetail').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
element: <KnowledgeBaseDetail />,
|
||||
loader: idLoader,
|
||||
path: 'bases/:id',
|
||||
},
|
||||
{
|
||||
lazy: () =>
|
||||
import('./(main)/knowledge/routes/KnowledgeBaseDetail').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
element: <KnowledgeBaseDetail />,
|
||||
loader: idLoader,
|
||||
path: '*',
|
||||
},
|
||||
],
|
||||
lazy: () =>
|
||||
import('./(main)/knowledge/_layout/Desktop').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
element: <KnowledgeLayout />,
|
||||
path: 'knowledge',
|
||||
},
|
||||
|
||||
@@ -240,17 +278,11 @@ export const createDesktopRouter = (locale: Locales) =>
|
||||
{
|
||||
children: [
|
||||
{
|
||||
element: <SettingsLayout />,
|
||||
index: true,
|
||||
lazy: () =>
|
||||
import('./(main)/settings/_layout/Desktop').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
},
|
||||
],
|
||||
lazy: () =>
|
||||
import('./(main)/settings/_layout/DesktopWrapper').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
element: <SettingsLayoutWrapper />,
|
||||
path: 'settings',
|
||||
},
|
||||
|
||||
@@ -258,26 +290,17 @@ export const createDesktopRouter = (locale: Locales) =>
|
||||
{
|
||||
children: [
|
||||
{
|
||||
element: <ImagePage />,
|
||||
index: true,
|
||||
lazy: () =>
|
||||
import('./(main)/image').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
},
|
||||
],
|
||||
lazy: () =>
|
||||
import('./(main)/image/_layout/DesktopWrapper').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
element: <ImageLayoutWrapper />,
|
||||
path: 'image',
|
||||
},
|
||||
|
||||
// Labs routes
|
||||
{
|
||||
lazy: () =>
|
||||
import('./(main)/labs').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
element: <LabsPage />,
|
||||
path: 'labs',
|
||||
},
|
||||
|
||||
@@ -285,45 +308,27 @@ export const createDesktopRouter = (locale: Locales) =>
|
||||
{
|
||||
children: [
|
||||
{
|
||||
element: <ProfileHomePage />,
|
||||
index: true,
|
||||
lazy: () =>
|
||||
import('./(main)/profile/(home)/desktop').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
},
|
||||
{
|
||||
lazy: () =>
|
||||
import('./(main)/profile/apikey/index').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
element: <ProfileApikeyPage />,
|
||||
path: 'apikey',
|
||||
},
|
||||
{
|
||||
lazy: () =>
|
||||
import('./(main)/profile/security/index').then((m) => ({
|
||||
Component: m.DesktopProfileSecurityPage,
|
||||
})),
|
||||
element: <DesktopProfileSecurityPage />,
|
||||
path: 'security',
|
||||
},
|
||||
{
|
||||
lazy: () =>
|
||||
import('./(main)/profile/stats/index').then((m) => ({
|
||||
Component: m.DesktopProfileStatsPage,
|
||||
})),
|
||||
element: <DesktopProfileStatsPage />,
|
||||
path: 'stats',
|
||||
},
|
||||
{
|
||||
lazy: () =>
|
||||
import('./(main)/profile/usage/index').then((m) => ({
|
||||
Component: m.DesktopProfileUsagePage,
|
||||
})),
|
||||
element: <DesktopProfileUsagePage />,
|
||||
path: 'usage',
|
||||
},
|
||||
],
|
||||
lazy: () =>
|
||||
import('./(main)/profile/_layout/DesktopWrapper').then((m) => ({
|
||||
Component: m.default,
|
||||
})),
|
||||
element: <ProfileLayoutWrapper />,
|
||||
path: 'profile',
|
||||
},
|
||||
|
||||
@@ -340,7 +345,6 @@ export const createDesktopRouter = (locale: Locales) =>
|
||||
},
|
||||
],
|
||||
element: <RootLayout locale={locale} />,
|
||||
loader: hydrationGateLoader,
|
||||
path: '/',
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1,15 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { type LoaderFunction, createBrowserRouter, redirect, useNavigate } from 'react-router-dom';
|
||||
import { createBrowserRouter, redirect, useNavigate } from 'react-router-dom';
|
||||
|
||||
import Loading from '@/components/Loading/BrandTextLoading';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import type { Locales } from '@/types/locale';
|
||||
|
||||
import { MobileMainLayout } from './(main)/layouts/mobile';
|
||||
import { idLoader, slugLoader } from './loaders/routeParams';
|
||||
|
||||
/**
|
||||
* Mobile Router Configuration - Pure CSR Mode
|
||||
*
|
||||
* IMPORTANT: This router runs ONLY in the browser (client-side).
|
||||
*
|
||||
* Key characteristics:
|
||||
* - createBrowserRouter uses window.history API (client-only)
|
||||
* - All loaders execute in the browser during navigation
|
||||
* - No server-side rendering or hydration involved
|
||||
* - Route data fetching happens on-demand during client navigation
|
||||
*
|
||||
* The entire router tree is wrapped with Next.js dynamic import (ssr: false),
|
||||
* ensuring this code never executes on the server.
|
||||
*/
|
||||
|
||||
// Component to register navigate function in global store
|
||||
const NavigatorRegistrar = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -34,16 +48,11 @@ const RootLayout = (props: { locale: Locales }) => (
|
||||
</>
|
||||
);
|
||||
|
||||
// Hydration gate loader -always return true to bypass hydration gate
|
||||
const hydrationGateLoader: LoaderFunction = () => {
|
||||
return true
|
||||
};
|
||||
|
||||
// Create mobile router configuration
|
||||
export const createMobileRouter = (locale: Locales) =>
|
||||
createBrowserRouter([
|
||||
{
|
||||
HydrateFallback: () => <Loading />,
|
||||
children: [
|
||||
// Chat routes
|
||||
{
|
||||
@@ -372,7 +381,6 @@ export const createMobileRouter = (locale: Locales) =>
|
||||
},
|
||||
],
|
||||
element: <RootLayout locale={locale} />,
|
||||
loader: hydrationGateLoader,
|
||||
path: '/',
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -63,16 +63,7 @@ const StoreInitialization = memo(() => {
|
||||
|
||||
// init user state
|
||||
useInitUserState(isLoginOnInit, serverConfig, {
|
||||
onError: () => {
|
||||
// 即使失败也要设置标志,避免应用卡住
|
||||
useGlobalStore.setState({ isAppHydrated: true });
|
||||
console.warn('[Hydration] Client state initialization failed.');
|
||||
},
|
||||
onSuccess: (state) => {
|
||||
// 设置水合完成标志
|
||||
useGlobalStore.setState({ isAppHydrated: true });
|
||||
console.log('[Hydration] Client state initialized successfully.');
|
||||
|
||||
if (state.isOnboard === false) {
|
||||
router.push('/onboard');
|
||||
}
|
||||
|
||||
@@ -122,12 +122,6 @@ export interface GlobalState {
|
||||
* 启动时为 Idle,完成为 Ready,报错为 Error
|
||||
*/
|
||||
initClientDBStage: DatabaseLoadingState;
|
||||
/**
|
||||
* 应用水合状态标志
|
||||
* 用于指示客户端状态是否已从 StoreInitialization 完成加载
|
||||
* 默认为 false,StoreInitialization 完成后设置为 true
|
||||
*/
|
||||
isAppHydrated: boolean;
|
||||
isMobile?: boolean;
|
||||
isStatusInit?: boolean;
|
||||
latestVersion?: string;
|
||||
@@ -170,7 +164,6 @@ export const INITIAL_STATUS = {
|
||||
|
||||
export const initialState: GlobalState = {
|
||||
initClientDBStage: DatabaseLoadingState.Idle,
|
||||
isAppHydrated: false,
|
||||
isMobile: false,
|
||||
isStatusInit: false,
|
||||
sidebarKey: SidebarTabKey.Chat,
|
||||
|
||||
Reference in New Issue
Block a user