mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-20 22:26:05 +00:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b8b1ab6616 | |||
| 3891015a3d | |||
| 5babb7d826 | |||
| a7504b696a | |||
| 9dc4308942 | |||
| 082117998d | |||
| 9a74d6c045 | |||
| b1a4f24dc9 | |||
| c47551775b | |||
| 2d83300795 | |||
| 0915538da8 | |||
| 53fc0642e0 | |||
| a8c725abd5 | |||
| b8a7f6e9eb | |||
| bb594f87e2 | |||
| b0ee9b434e | |||
| cf2c5a1d37 | |||
| 0511e43a48 | |||
| 1f128f407f | |||
| f258a2e042 | |||
| 7996e1c431 | |||
| 93dddfc2e5 | |||
| 5e4186559b | |||
| 9bfd9bb4a5 | |||
| 9ca54135b5 | |||
| f162556607 | |||
| 3292ed83f9 | |||
| 561a38f788 | |||
| 71aaf0fac5 | |||
| 287601f8ec | |||
| b36f8781e6 | |||
| 705450a571 | |||
| 5272c7373f | |||
| fb24b6f1b7 | |||
| 2fd65fe8a3 | |||
| 35d5a2c937 | |||
| 42f40d2717 | |||
| ef8a644d8c | |||
| 81c84348bc | |||
| 8d7a0467db | |||
| e9522729c5 | |||
| cf01894077 | |||
| b5d945b1fd | |||
| cbee964582 | |||
| 87a38ad0c4 | |||
| f2d4745ad3 | |||
| 0167ac8e28 | |||
| b480227fd0 | |||
| 97ff98cada | |||
| 845d3ef58a | |||
| 906917362f | |||
| c69049d6da | |||
| 4f7356ffab | |||
| d20c82c115 | |||
| d617a6cd97 | |||
| 408391eeb6 | |||
| 4a2e671f55 | |||
| 695a261df1 | |||
| 39b723eff4 | |||
| 68937d842c | |||
| b66bc66260 | |||
| 4d06279abd | |||
| 1a8d33fbf4 | |||
| 2c086373cc | |||
| c7d49258f8 | |||
| 2280fd6ff9 | |||
| 8eb901c401 |
+5
-5
@@ -249,11 +249,11 @@ const nextConfig: NextConfig = {
|
|||||||
// permanent: true,
|
// permanent: true,
|
||||||
// source: '/settings',
|
// source: '/settings',
|
||||||
// },
|
// },
|
||||||
{
|
// {
|
||||||
destination: '/chat',
|
// destination: '/chat',
|
||||||
permanent: false,
|
// permanent: false,
|
||||||
source: '/',
|
// source: '/',
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
destination: '/chat',
|
destination: '/chat',
|
||||||
permanent: true,
|
permanent: true,
|
||||||
|
|||||||
+1
-1
@@ -397,4 +397,4 @@
|
|||||||
"@vercel/speed-insights"
|
"@vercel/speed-insights"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ const partialBuildPages = [
|
|||||||
{
|
{
|
||||||
name: 'changelog',
|
name: 'changelog',
|
||||||
disabled: isDesktop,
|
disabled: isDesktop,
|
||||||
paths: ['src/app/[variants]/@modal/(.)changelog', 'src/app/[variants]/(main)/changelog'],
|
paths: ['src/app/[variants]/(main)/changelog'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'auth',
|
name: 'auth',
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ const handler = (req: NextRequest) => {
|
|||||||
*/
|
*/
|
||||||
createContext: () => createLambdaContext(req),
|
createContext: () => createLambdaContext(req),
|
||||||
|
|
||||||
endpoint: '/trpc/desktop',
|
endpoint: '/trpc/desktop',
|
||||||
|
|
||||||
onError: ({ error, path, type }) => {
|
onError: ({ error, path, type }) => {
|
||||||
pino.info(`Error in tRPC handler (desktop) on path: ${path}, type: ${type}`);
|
pino.info(`Error in tRPC handler (desktop) on path: ${path}, type: ${type}`);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
},
|
},
|
||||||
|
|
||||||
req: preparedReq,
|
req: preparedReq,
|
||||||
responseMeta({ ctx }) {
|
responseMeta({ ctx }) {
|
||||||
@@ -34,4 +34,4 @@ const handler = (req: NextRequest) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export { handler as GET, handler as POST };
|
export { handler as GET, handler as POST };
|
||||||
@@ -6,10 +6,12 @@ import { useUserStore } from '@/store/user';
|
|||||||
import UserBanner from '../features/UserBanner';
|
import UserBanner from '../features/UserBanner';
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
vi.mock('next/navigation', () => ({
|
const mockNavigate = vi.fn();
|
||||||
useRouter: vi.fn(() => ({
|
vi.mock('react-router-dom', () => ({
|
||||||
push: vi.fn(),
|
Link: ({ to, children }: { to: string; children: React.ReactNode }) => (
|
||||||
})),
|
<a href={to}>{children}</a>
|
||||||
|
),
|
||||||
|
useNavigate: () => mockNavigate,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@/features/User/UserInfo', () => ({
|
vi.mock('@/features/User/UserInfo', () => ({
|
||||||
@@ -45,6 +47,7 @@ vi.mock('@/const/auth', () => ({
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
enableAuth = true;
|
enableAuth = true;
|
||||||
|
mockNavigate.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('UserBanner', () => {
|
describe('UserBanner', () => {
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ const wrapper: React.JSXElementConstructor<{ children: React.ReactNode }> = ({ c
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
vi.mock('next/navigation', () => ({
|
const mockNavigate = vi.fn();
|
||||||
useRouter: vi.fn(() => ({
|
vi.mock('react-router-dom', () => ({
|
||||||
push: vi.fn(),
|
useNavigate: () => mockNavigate,
|
||||||
})),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('react-i18next', () => ({
|
vi.mock('react-i18next', () => ({
|
||||||
@@ -48,6 +47,7 @@ vi.mock('@/const/version', async (importOriginal) => {
|
|||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
enableAuth = true;
|
enableAuth = true;
|
||||||
enableClerk = true;
|
enableClerk = true;
|
||||||
|
mockNavigate.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useCategory', () => {
|
describe('useCategory', () => {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { Flexbox } from 'react-layout-kit';
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
import { enableAuth, enableNextAuth } from '@/const/auth';
|
import { enableAuth, enableNextAuth } from '@/const/auth';
|
||||||
@@ -13,7 +12,7 @@ import { useUserStore } from '@/store/user';
|
|||||||
import { authSelectors } from '@/store/user/selectors';
|
import { authSelectors } from '@/store/user/selectors';
|
||||||
|
|
||||||
const UserBanner = memo(() => {
|
const UserBanner = memo(() => {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const isLoginWithAuth = useUserStore(authSelectors.isLoginWithAuth);
|
const isLoginWithAuth = useUserStore(authSelectors.isLoginWithAuth);
|
||||||
const [signIn] = useUserStore((s) => [s.openLogin]);
|
const [signIn] = useUserStore((s) => [s.openLogin]);
|
||||||
|
|
||||||
@@ -21,10 +20,10 @@ const UserBanner = memo(() => {
|
|||||||
<Flexbox gap={12} paddingBlock={8}>
|
<Flexbox gap={12} paddingBlock={8}>
|
||||||
{!enableAuth || (enableAuth && isLoginWithAuth) ? (
|
{!enableAuth || (enableAuth && isLoginWithAuth) ? (
|
||||||
<>
|
<>
|
||||||
<Link href={'/profile'} style={{ color: 'inherit' }}>
|
<Link style={{ color: 'inherit' }} to="/profile">
|
||||||
<UserInfo />
|
<UserInfo />
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={'/profile/stats'} style={{ color: 'inherit' }}>
|
<Link style={{ color: 'inherit' }} to="/profile/stats">
|
||||||
<DataStatistics paddingInline={12} />
|
<DataStatistics paddingInline={12} />
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
@@ -36,7 +35,7 @@ const UserBanner = memo(() => {
|
|||||||
signIn();
|
signIn();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.push('/login');
|
navigate('/login');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
FileClockIcon,
|
FileClockIcon,
|
||||||
Settings2,
|
Settings2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { CellProps } from '@/components/Cell';
|
import { CellProps } from '@/components/Cell';
|
||||||
import { usePWAInstall } from '@/hooks/usePWAInstall';
|
import { usePWAInstall } from '@/hooks/usePWAInstall';
|
||||||
@@ -18,7 +18,7 @@ import { useUserStore } from '@/store/user';
|
|||||||
import { authSelectors } from '@/store/user/selectors';
|
import { authSelectors } from '@/store/user/selectors';
|
||||||
|
|
||||||
export const useCategory = () => {
|
export const useCategory = () => {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const { canInstall, install } = usePWAInstall();
|
const { canInstall, install } = usePWAInstall();
|
||||||
const { t } = useTranslation(['common', 'setting', 'auth']);
|
const { t } = useTranslation(['common', 'setting', 'auth']);
|
||||||
const { showCloudPromotion, hideDocs } = useServerConfigStore(featureFlagsSelectors);
|
const { showCloudPromotion, hideDocs } = useServerConfigStore(featureFlagsSelectors);
|
||||||
@@ -29,7 +29,7 @@ export const useCategory = () => {
|
|||||||
icon: CircleUserRound,
|
icon: CircleUserRound,
|
||||||
key: 'profile',
|
key: 'profile',
|
||||||
label: t('userPanel.profile'),
|
label: t('userPanel.profile'),
|
||||||
onClick: () => router.push('/me/profile'),
|
onClick: () => navigate('/me/profile'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ export const useCategory = () => {
|
|||||||
icon: Settings2,
|
icon: Settings2,
|
||||||
key: 'setting',
|
key: 'setting',
|
||||||
label: t('userPanel.setting'),
|
label: t('userPanel.setting'),
|
||||||
onClick: () => router.push('/me/settings'),
|
onClick: () => navigate('/me/settings'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
@@ -58,9 +58,6 @@ export const useCategory = () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
/* ↓ cloud slot ↓ */
|
/* ↓ cloud slot ↓ */
|
||||||
|
|
||||||
/* ↑ cloud slot ↑ */
|
|
||||||
|
|
||||||
const helps: CellProps[] = [
|
const helps: CellProps[] = [
|
||||||
showCloudPromotion && {
|
showCloudPromotion && {
|
||||||
icon: Cloudy,
|
icon: Cloudy,
|
||||||
@@ -84,7 +81,7 @@ export const useCategory = () => {
|
|||||||
icon: FileClockIcon,
|
icon: FileClockIcon,
|
||||||
key: 'changelog',
|
key: 'changelog',
|
||||||
label: t('changelog'),
|
label: t('changelog'),
|
||||||
onClick: () => router.push('/changelog'),
|
onClick: () => navigate('/changelog'),
|
||||||
},
|
},
|
||||||
].filter(Boolean) as CellProps[];
|
].filter(Boolean) as CellProps[];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { Center } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import BrandWatermark from '@/components/BrandWatermark';
|
||||||
|
|
||||||
|
import Category from './features/Category';
|
||||||
|
import UserBanner from './features/UserBanner';
|
||||||
|
|
||||||
|
const MeHomePage = memo(() => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UserBanner />
|
||||||
|
<Category />
|
||||||
|
<Center padding={16}>
|
||||||
|
<BrandWatermark />
|
||||||
|
</Center>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
MeHomePage.displayName = 'MeHomePage';
|
||||||
|
|
||||||
|
export default MeHomePage;
|
||||||
@@ -1,18 +1,15 @@
|
|||||||
import { PropsWithChildren, Suspense } from 'react';
|
import MobileContentLayout from "@/components/server/MobileNavLayout";
|
||||||
|
import Loading from "@/components/Loading/BrandTextLoading";
|
||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
import Header from "./features/Header";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
import Loading from '@/components/Loading/BrandTextLoading';
|
const Layout = () => {
|
||||||
import MobileContentLayout from '@/components/server/MobileNavLayout';
|
return <MobileContentLayout header={<Header />} withNav>
|
||||||
|
<Suspense fallback={<Loading />}>
|
||||||
import Header from './features/Header';
|
<Outlet />
|
||||||
|
</Suspense>
|
||||||
const Layout = ({ children }: PropsWithChildren) => {
|
|
||||||
return (
|
|
||||||
<MobileContentLayout header={<Header />} withNav>
|
|
||||||
<Suspense fallback={<Loading />}>{children}</Suspense>
|
|
||||||
</MobileContentLayout>
|
</MobileContentLayout>
|
||||||
);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
Layout.displayName = 'MeLayout';
|
export default Layout;
|
||||||
|
|
||||||
export default Layout;
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Skeleton } from 'antd';
|
|
||||||
import { memo } from 'react';
|
|
||||||
import { Flexbox } from 'react-layout-kit';
|
|
||||||
|
|
||||||
import Divider from '@/components/Cell/Divider';
|
|
||||||
import SkeletonLoading from '@/components/Loading/SkeletonLoading';
|
|
||||||
|
|
||||||
const Loading = memo(() => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Flexbox align={'center'} gap={12} horizontal paddingBlock={12} paddingInline={12}>
|
|
||||||
<Skeleton.Avatar active shape={'circle'} size={48} />
|
|
||||||
<Skeleton.Button active block />
|
|
||||||
</Flexbox>
|
|
||||||
<Flexbox gap={4} horizontal paddingBlock={12} paddingInline={16}>
|
|
||||||
<Skeleton.Button active block />
|
|
||||||
<Skeleton.Button active block />
|
|
||||||
<Skeleton.Button active block />
|
|
||||||
</Flexbox>
|
|
||||||
<Divider />
|
|
||||||
<SkeletonLoading
|
|
||||||
active
|
|
||||||
paragraph={{ rows: 6, style: { marginBottom: 0 }, width: '100%' }}
|
|
||||||
title={false}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
<SkeletonLoading
|
|
||||||
active
|
|
||||||
paragraph={{ rows: 3, style: { marginBottom: 0 }, width: '100%' }}
|
|
||||||
title={false}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default Loading;
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { redirect } from 'next/navigation';
|
|
||||||
import { Center } from 'react-layout-kit';
|
|
||||||
|
|
||||||
import BrandWatermark from '@/components/BrandWatermark';
|
|
||||||
import { metadataModule } from '@/server/metadata';
|
|
||||||
import { translation } from '@/server/translation';
|
|
||||||
import { DynamicLayoutProps } from '@/types/next';
|
|
||||||
import { RouteVariants } from '@/utils/server/routeVariants';
|
|
||||||
|
|
||||||
import Category from './features/Category';
|
|
||||||
import UserBanner from './features/UserBanner';
|
|
||||||
|
|
||||||
export const generateMetadata = async (props: DynamicLayoutProps) => {
|
|
||||||
const locale = await RouteVariants.getLocale(props);
|
|
||||||
const { t } = await translation('common', locale);
|
|
||||||
return metadataModule.generate({
|
|
||||||
title: t('tab.me'),
|
|
||||||
url: '/me',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const Page = async (props: DynamicLayoutProps) => {
|
|
||||||
const isMobile = await RouteVariants.getIsMobile(props);
|
|
||||||
|
|
||||||
if (!isMobile) return redirect('/chat');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<UserBanner />
|
|
||||||
<Category />
|
|
||||||
<Center padding={16}>
|
|
||||||
<BrandWatermark />
|
|
||||||
</Center>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Page.displayName = 'Me';
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChartColumnBigIcon, LogOut, ShieldCheck, UserCircle } from 'lucide-react';
|
import { ChartColumnBigIcon, LogOut, ShieldCheck, UserCircle } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import Cell, { CellProps } from '@/components/Cell';
|
import Cell, { CellProps } from '@/components/Cell';
|
||||||
import { ProfileTabs } from '@/store/global/initialState';
|
import { ProfileTabs } from '@/store/global/initialState';
|
||||||
@@ -16,26 +16,26 @@ const Category = memo(() => {
|
|||||||
authSelectors.isLoginWithClerk(s),
|
authSelectors.isLoginWithClerk(s),
|
||||||
s.logout,
|
s.logout,
|
||||||
]);
|
]);
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation('auth');
|
const { t } = useTranslation('auth');
|
||||||
const items: CellProps[] = [
|
const items: CellProps[] = [
|
||||||
{
|
{
|
||||||
icon: UserCircle,
|
icon: UserCircle,
|
||||||
key: ProfileTabs.Profile,
|
key: ProfileTabs.Profile,
|
||||||
label: t('tab.profile'),
|
label: t('tab.profile'),
|
||||||
onClick: () => router.push('/profile'),
|
onClick: () => navigate('/profile'),
|
||||||
},
|
},
|
||||||
isLoginWithClerk && {
|
isLoginWithClerk && {
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
key: ProfileTabs.Security,
|
key: ProfileTabs.Security,
|
||||||
label: t('tab.security'),
|
label: t('tab.security'),
|
||||||
onClick: () => router.push('/profile/security'),
|
onClick: () => navigate('/profile/security'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: ChartColumnBigIcon,
|
icon: ChartColumnBigIcon,
|
||||||
key: ProfileTabs.Stats,
|
key: ProfileTabs.Stats,
|
||||||
label: t('tab.stats'),
|
label: t('tab.stats'),
|
||||||
onClick: () => router.push('/profile/stats'),
|
onClick: () => navigate('/profile/stats'),
|
||||||
},
|
},
|
||||||
isLogin && {
|
isLogin && {
|
||||||
type: 'divider',
|
type: 'divider',
|
||||||
@@ -46,7 +46,7 @@ const Category = memo(() => {
|
|||||||
label: t('signout', { ns: 'auth' }),
|
label: t('signout', { ns: 'auth' }),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
signOut();
|
signOut();
|
||||||
router.push('/login');
|
navigate('/login');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
].filter(Boolean) as CellProps[];
|
].filter(Boolean) as CellProps[];
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChatHeader } from '@lobehub/ui/mobile';
|
import { ChatHeader } from '@lobehub/ui/mobile';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Flexbox } from 'react-layout-kit';
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
import { mobileHeaderSticky } from '@/styles/mobileHeader';
|
import { mobileHeaderSticky } from '@/styles/mobileHeader';
|
||||||
@@ -11,7 +11,7 @@ import { mobileHeaderSticky } from '@/styles/mobileHeader';
|
|||||||
const Header = memo(() => {
|
const Header = memo(() => {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
<ChatHeader
|
<ChatHeader
|
||||||
center={
|
center={
|
||||||
@@ -23,7 +23,7 @@ const Header = memo(() => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onBackClick={() => router.push('/me')}
|
onBackClick={() => navigate('/me')}
|
||||||
showBackButton
|
showBackButton
|
||||||
style={mobileHeaderSticky}
|
style={mobileHeaderSticky}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
import Category from './features/Category';
|
||||||
|
|
||||||
|
const MeProfilePage = memo(() => {
|
||||||
|
return (
|
||||||
|
<Category />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
MeProfilePage.displayName = 'MeProfilePage';
|
||||||
|
|
||||||
|
export default MeProfilePage;
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import MobileContentLayout from "@/components/server/MobileNavLayout";
|
||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
import Header from "./features/Header";
|
||||||
|
|
||||||
import MobileContentLayout from '@/components/server/MobileNavLayout';
|
const Layout = () => {
|
||||||
|
return <MobileContentLayout header={<Header />}>
|
||||||
|
<Outlet />
|
||||||
|
</MobileContentLayout>
|
||||||
|
}
|
||||||
|
|
||||||
import Header from './features/Header';
|
export default Layout;
|
||||||
|
|
||||||
const Layout = ({ children }: PropsWithChildren) => {
|
|
||||||
return <MobileContentLayout header={<Header />}>{children}</MobileContentLayout>;
|
|
||||||
};
|
|
||||||
|
|
||||||
Layout.displayName = 'MeProfileLayout';
|
|
||||||
|
|
||||||
export default Layout;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import SkeletonLoading from '@/components/Loading/SkeletonLoading';
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
return <SkeletonLoading paragraph={{ rows: 8 }} />;
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
import { metadataModule } from '@/server/metadata';
|
|
||||||
import { translation } from '@/server/translation';
|
|
||||||
import { DynamicLayoutProps } from '@/types/next';
|
|
||||||
import { RouteVariants } from '@/utils/server/routeVariants';
|
|
||||||
|
|
||||||
import Category from './features/Category';
|
|
||||||
|
|
||||||
export const generateMetadata = async (props: DynamicLayoutProps) => {
|
|
||||||
const locale = await RouteVariants.getLocale(props);
|
|
||||||
const { t } = await translation('auth', locale);
|
|
||||||
return metadataModule.generate({
|
|
||||||
description: t('header.desc'),
|
|
||||||
title: t('header.title'),
|
|
||||||
url: '/me/profile',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const Page = async (props: DynamicLayoutProps) => {
|
|
||||||
const isMobile = await RouteVariants.getIsMobile(props);
|
|
||||||
|
|
||||||
if (!isMobile) return redirect('/profile');
|
|
||||||
|
|
||||||
return <Category />;
|
|
||||||
};
|
|
||||||
|
|
||||||
Page.displayName = 'MeProfile';
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChatHeader } from '@lobehub/ui/mobile';
|
import { ChatHeader } from '@lobehub/ui/mobile';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Flexbox } from 'react-layout-kit';
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
import { mobileHeaderSticky } from '@/styles/mobileHeader';
|
import { mobileHeaderSticky } from '@/styles/mobileHeader';
|
||||||
@@ -11,7 +11,7 @@ import { mobileHeaderSticky } from '@/styles/mobileHeader';
|
|||||||
const Header = memo(() => {
|
const Header = memo(() => {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation('common');
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
<ChatHeader
|
<ChatHeader
|
||||||
center={
|
center={
|
||||||
@@ -23,7 +23,7 @@ const Header = memo(() => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onBackClick={() => router.push('/me')}
|
onBackClick={() => navigate('/me')}
|
||||||
showBackButton
|
showBackButton
|
||||||
style={mobileHeaderSticky}
|
style={mobileHeaderSticky}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { Bot, Brain, Info, Mic2, Settings2, Sparkles } from 'lucide-react';
|
import { Bot, Brain, Info, Mic2, Settings2, Sparkles } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { CellProps } from '@/components/Cell';
|
import { CellProps } from '@/components/Cell';
|
||||||
import { SettingsTabs } from '@/store/global/initialState';
|
import { SettingsTabs } from '@/store/global/initialState';
|
||||||
|
|
||||||
export const useCategory = () => {
|
export const useCategory = () => {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation('setting');
|
const { t } = useTranslation('setting');
|
||||||
|
|
||||||
const items: CellProps[] = [
|
const items: CellProps[] = [
|
||||||
@@ -40,6 +39,6 @@ export const useCategory = () => {
|
|||||||
|
|
||||||
return items.map((item) => ({
|
return items.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
onClick: () => router.push(`/settings?active=${item.key}`),
|
onClick: () => navigate(`/settings?active=${item.key}`),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
|
||||||
|
import Category from './features/Category';
|
||||||
|
|
||||||
|
const MeSettingsPage = memo(() => {
|
||||||
|
return (
|
||||||
|
<Category />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
MeSettingsPage.displayName = 'MeSettingsPage';
|
||||||
|
|
||||||
|
export default MeSettingsPage;
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
import MobileContentLayout from "@/components/server/MobileNavLayout";
|
||||||
|
import Loading from "@/components/Loading/BrandTextLoading";
|
||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
import Header from "./features/Header";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
import MobileContentLayout from '@/components/server/MobileNavLayout';
|
const Layout = () => {
|
||||||
|
return <MobileContentLayout header={<Header />} withNav>
|
||||||
|
<Suspense fallback={<Loading />}>
|
||||||
|
<Outlet />
|
||||||
|
</Suspense>
|
||||||
|
</MobileContentLayout>
|
||||||
|
}
|
||||||
|
|
||||||
import Header from './features/Header';
|
export default Layout;
|
||||||
|
|
||||||
const Layout = ({ children }: PropsWithChildren) => {
|
|
||||||
return <MobileContentLayout header={<Header />}>{children}</MobileContentLayout>;
|
|
||||||
};
|
|
||||||
|
|
||||||
Layout.displayName = 'MeSettingsLayout';
|
|
||||||
|
|
||||||
export default Layout;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import SkeletonLoading from '@/components/Loading/SkeletonLoading';
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
return <SkeletonLoading paragraph={{ rows: 8 }} />;
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
import { metadataModule } from '@/server/metadata';
|
|
||||||
import { translation } from '@/server/translation';
|
|
||||||
import { DynamicLayoutProps } from '@/types/next';
|
|
||||||
import { RouteVariants } from '@/utils/server/routeVariants';
|
|
||||||
|
|
||||||
import Category from './features/Category';
|
|
||||||
|
|
||||||
export const generateMetadata = async (props: DynamicLayoutProps) => {
|
|
||||||
const locale = await RouteVariants.getLocale(props);
|
|
||||||
const { t } = await translation('setting', locale);
|
|
||||||
return metadataModule.generate({
|
|
||||||
description: t('header.desc'),
|
|
||||||
title: t('header.title'),
|
|
||||||
url: '/me/settings',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const Page = async (props: DynamicLayoutProps) => {
|
|
||||||
const isMobile = await RouteVariants.getIsMobile(props);
|
|
||||||
|
|
||||||
if (!isMobile) return redirect('/settings');
|
|
||||||
|
|
||||||
return <Category />;
|
|
||||||
};
|
|
||||||
|
|
||||||
Page.displayName = 'MeSettings';
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
import { ActionIcon, ActionIconProps, Hotkey } from '@lobehub/ui';
|
|
||||||
import { Compass, FolderClosed, MessageSquare, Palette } from 'lucide-react';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { memo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { Flexbox } from 'react-layout-kit';
|
|
||||||
|
|
||||||
import { useGlobalStore } from '@/store/global';
|
|
||||||
import { SidebarTabKey } from '@/store/global/initialState';
|
|
||||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
|
||||||
import { useSessionStore } from '@/store/session';
|
|
||||||
import { useUserStore } from '@/store/user';
|
|
||||||
import { settingsSelectors } from '@/store/user/selectors';
|
|
||||||
import { HotkeyEnum } from '@/types/hotkey';
|
|
||||||
|
|
||||||
const ICON_SIZE: ActionIconProps['size'] = {
|
|
||||||
blockSize: 40,
|
|
||||||
size: 24,
|
|
||||||
strokeWidth: 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface TopActionProps {
|
|
||||||
isPinned?: boolean | null;
|
|
||||||
tab?: SidebarTabKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Change icons
|
|
||||||
const TopActions = memo<TopActionProps>(({ tab, isPinned }) => {
|
|
||||||
const { t } = useTranslation('common');
|
|
||||||
const switchBackToChat = useGlobalStore((s) => s.switchBackToChat);
|
|
||||||
const { showMarket, enableKnowledgeBase, showAiImage } =
|
|
||||||
useServerConfigStore(featureFlagsSelectors);
|
|
||||||
const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.NavigateToChat));
|
|
||||||
|
|
||||||
const isChatActive = tab === SidebarTabKey.Chat && !isPinned;
|
|
||||||
const isFilesActive = tab === SidebarTabKey.Files;
|
|
||||||
const isDiscoverActive = tab === SidebarTabKey.Discover;
|
|
||||||
const isImageActive = tab === SidebarTabKey.Image;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flexbox gap={8}>
|
|
||||||
<Link
|
|
||||||
aria-label={t('tab.chat')}
|
|
||||||
href={'/chat'}
|
|
||||||
onClick={(e) => {
|
|
||||||
// If Cmd key is pressed, let the default link behavior happen (open in new tab)
|
|
||||||
if (e.metaKey || e.ctrlKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, prevent default and switch session within the current tab
|
|
||||||
e.preventDefault();
|
|
||||||
switchBackToChat(useSessionStore.getState().activeId);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ActionIcon
|
|
||||||
active={isChatActive}
|
|
||||||
icon={MessageSquare}
|
|
||||||
size={ICON_SIZE}
|
|
||||||
title={
|
|
||||||
<Flexbox align={'center'} gap={8} horizontal justify={'space-between'}>
|
|
||||||
<span>{t('tab.chat')}</span>
|
|
||||||
<Hotkey inverseTheme keys={hotkey} />
|
|
||||||
</Flexbox>
|
|
||||||
}
|
|
||||||
tooltipProps={{ placement: 'right' }}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
{enableKnowledgeBase && (
|
|
||||||
<Link aria-label={t('tab.knowledgeBase')} href={'/knowledge'}>
|
|
||||||
<ActionIcon
|
|
||||||
active={isFilesActive}
|
|
||||||
icon={FolderClosed}
|
|
||||||
size={ICON_SIZE}
|
|
||||||
title={t('tab.knowledgeBase')}
|
|
||||||
tooltipProps={{ placement: 'right' }}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{showAiImage && (
|
|
||||||
<Link aria-label={t('tab.aiImage')} href={'/image'}>
|
|
||||||
<ActionIcon
|
|
||||||
active={isImageActive}
|
|
||||||
icon={Palette}
|
|
||||||
size={ICON_SIZE}
|
|
||||||
title={t('tab.aiImage')}
|
|
||||||
tooltipProps={{ placement: 'right' }}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{showMarket && (
|
|
||||||
<Link aria-label={t('tab.discover')} href={'/discover'}>
|
|
||||||
<ActionIcon
|
|
||||||
active={isDiscoverActive}
|
|
||||||
icon={Compass}
|
|
||||||
size={ICON_SIZE}
|
|
||||||
title={t('tab.discover')}
|
|
||||||
tooltipProps={{ placement: 'right' }}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</Flexbox>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default TopActions;
|
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
import { ReactNode } from 'react';
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { Locales } from '@/locales/resources';
|
||||||
|
|
||||||
import Hero from '../../features/Hero';
|
import Hero from '../../features/Hero';
|
||||||
import Container from './Container';
|
import Container from './Container';
|
||||||
|
|
||||||
type Props = { children: ReactNode };
|
const Layout = (props: { locale: Locales }) => {
|
||||||
|
const { locale } = props;
|
||||||
const Layout = ({ children }: Props) => {
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Hero />
|
<Hero />
|
||||||
{children}
|
<Outlet context={{ locale }} />
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChatHeader } from '@lobehub/ui/mobile';
|
import { ChatHeader } from '@lobehub/ui/mobile';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Flexbox } from 'react-layout-kit';
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { mobileHeaderSticky } from '@/styles/mobileHeader';
|
import { mobileHeaderSticky } from '@/styles/mobileHeader';
|
||||||
|
|
||||||
const Header = memo(() => {
|
const Header = memo(() => {
|
||||||
const { t } = useTranslation('changelog');
|
const { t } = useTranslation('changelog');
|
||||||
|
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
<ChatHeader
|
<ChatHeader
|
||||||
center={
|
center={
|
||||||
@@ -23,7 +23,7 @@ const Header = memo(() => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onBackClick={() => router.back()}
|
onBackClick={() => navigate(-1)}
|
||||||
showBackButton
|
showBackButton
|
||||||
style={mobileHeaderSticky}
|
style={mobileHeaderSticky}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { ReactNode } from 'react';
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
import MobileContentLayout from '@/components/server/MobileNavLayout';
|
import MobileContentLayout from '@/components/server/MobileNavLayout';
|
||||||
|
import { Locales } from '@/locales/resources';
|
||||||
|
|
||||||
import Hero from '../../features/Hero';
|
import Hero from '../../features/Hero';
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
|
|
||||||
type Props = { children: ReactNode };
|
const Layout = (props: { locale: Locales }) => {
|
||||||
|
const { locale } = props;
|
||||||
const Layout = ({ children }: Props) => {
|
|
||||||
return (
|
return (
|
||||||
<MobileContentLayout header={<Header />} padding={16}>
|
<MobileContentLayout header={<Header />} padding={16}>
|
||||||
<Hero />
|
<Hero />
|
||||||
{children}
|
<Outlet context={{ locale }} />
|
||||||
</MobileContentLayout>
|
</MobileContentLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Typography } from '@lobehub/ui';
|
import { Typography } from '@lobehub/ui';
|
||||||
|
import { Image } from '@lobehub/ui/mdx';
|
||||||
import { Divider } from 'antd';
|
import { Divider } from 'antd';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import useSWR from 'swr';
|
||||||
import urlJoin from 'url-join';
|
import urlJoin from 'url-join';
|
||||||
|
|
||||||
import { CustomMDX } from '@/components/mdx';
|
import { CustomMDX } from '@/components/mdx';
|
||||||
import Image from '@/components/mdx/Image';
|
|
||||||
import { OFFICIAL_SITE } from '@/const/url';
|
import { OFFICIAL_SITE } from '@/const/url';
|
||||||
import { Locales } from '@/locales/resources';
|
import { Locales } from '@/locales/resources';
|
||||||
import { ChangelogService } from '@/server/services/changelog';
|
import { ChangelogService } from '@/server/services/changelog';
|
||||||
@@ -14,14 +15,16 @@ import GridLayout from './GridLayout';
|
|||||||
import PublishedTime from './PublishedTime';
|
import PublishedTime from './PublishedTime';
|
||||||
import VersionTag from './VersionTag';
|
import VersionTag from './VersionTag';
|
||||||
|
|
||||||
const Post = async ({
|
const Post = ({
|
||||||
id,
|
id,
|
||||||
mobile,
|
mobile,
|
||||||
versionRange,
|
versionRange,
|
||||||
locale,
|
locale,
|
||||||
}: ChangelogIndexItem & { branch?: string; locale: Locales; mobile?: boolean }) => {
|
}: ChangelogIndexItem & { branch?: string; locale: Locales; mobile?: boolean }) => {
|
||||||
const changelogService = new ChangelogService();
|
const { data } = useSWR([`changelog-post-${id}`, locale], async () => {
|
||||||
const data = await changelogService.getPostById(id, { locale });
|
const changelogService = new ChangelogService();
|
||||||
|
return await changelogService.getPostById(id, { locale });
|
||||||
|
});
|
||||||
|
|
||||||
if (!data || !data.title) return null;
|
if (!data || !data.title) return null;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Fragment } from 'react';
|
||||||
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
import { useOutletContext } from 'react-router-dom';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
|
import NotFound from '@/components/404';
|
||||||
|
import { Locales } from '@/locales/resources';
|
||||||
|
import { ChangelogService } from '@/server/services/changelog';
|
||||||
|
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||||
|
|
||||||
|
import GridLayout from './features/GridLayout';
|
||||||
|
import Pagination from './features/Pagination';
|
||||||
|
import Post from './features/Post';
|
||||||
|
import UpdateChangelogStatus from './features/UpdateChangelogStatus';
|
||||||
|
|
||||||
|
const Page = (props: { isMobile: boolean }) => {
|
||||||
|
const { locale } = useOutletContext<{ locale: Locales }>();
|
||||||
|
const { isMobile } = props;
|
||||||
|
const { hideDocs } = useServerConfigStore(featureFlagsSelectors);
|
||||||
|
|
||||||
|
const { data } = useSWR('changelog-index', async () => {
|
||||||
|
const changelogService = new ChangelogService();
|
||||||
|
return await changelogService.getChangelogIndex();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hideDocs) return <NotFound />;
|
||||||
|
|
||||||
|
if (!data) return <NotFound />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flexbox gap={isMobile ? 16 : 48}>
|
||||||
|
{data?.map((item) => (
|
||||||
|
<Fragment key={item.id}>
|
||||||
|
<Post locale={locale} mobile={isMobile} {...item} />
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</Flexbox>
|
||||||
|
<GridLayout>
|
||||||
|
<Pagination />
|
||||||
|
</GridLayout>
|
||||||
|
<UpdateChangelogStatus currentId={data[0]?.id} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DesktopPage = () => {
|
||||||
|
return <Page isMobile={false} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MobilePage = () => {
|
||||||
|
return <Page isMobile={true} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { DesktopPage, MobilePage };
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import ServerLayout from '@/components/server/ServerLayout';
|
|
||||||
|
|
||||||
import Desktop from './_layout/Desktop';
|
|
||||||
import Mobile from './_layout/Mobile';
|
|
||||||
|
|
||||||
const MainLayout = ServerLayout({ Desktop, Mobile });
|
|
||||||
|
|
||||||
MainLayout.displayName = 'ChangelogLayout';
|
|
||||||
|
|
||||||
export default MainLayout;
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
import { useQueryRoute } from '@/hooks/useQueryRoute';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description: Changelog Modal (intercepting routes fallback when hard refresh)
|
|
||||||
* @example: /changelog/modal => /changelog
|
|
||||||
* @refs: https://github.com/lobehub/lobe-chat/discussions/2295#discussioncomment-9290942
|
|
||||||
*/
|
|
||||||
|
|
||||||
const ChangelogModal = () => {
|
|
||||||
const router = useQueryRoute();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
router.replace('/changelog');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ChangelogModal;
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { Divider, Skeleton } from 'antd';
|
|
||||||
import { notFound } from 'next/navigation';
|
|
||||||
import { Fragment, Suspense } from 'react';
|
|
||||||
import { Flexbox } from 'react-layout-kit';
|
|
||||||
import urlJoin from 'url-join';
|
|
||||||
|
|
||||||
import Pagination from '@/app/[variants]/@modal/(.)changelog/modal/features/Pagination';
|
|
||||||
import UpdateChangelogStatus from '@/app/[variants]/@modal/(.)changelog/modal/features/UpdateChangelogStatus';
|
|
||||||
import StructuredData from '@/components/StructuredData';
|
|
||||||
import { serverFeatureFlags } from '@/config/featureFlags';
|
|
||||||
import { BRANDING_NAME } from '@/const/branding';
|
|
||||||
import { OFFICIAL_SITE } from '@/const/url';
|
|
||||||
import { ldModule } from '@/server/ld';
|
|
||||||
import { metadataModule } from '@/server/metadata';
|
|
||||||
import { ChangelogService } from '@/server/services/changelog';
|
|
||||||
import { translation } from '@/server/translation';
|
|
||||||
import { DynamicLayoutProps } from '@/types/next';
|
|
||||||
import { RouteVariants } from '@/utils/server/routeVariants';
|
|
||||||
|
|
||||||
import GridLayout from './features/GridLayout';
|
|
||||||
import Post from './features/Post';
|
|
||||||
|
|
||||||
export const generateMetadata = async (props: DynamicLayoutProps) => {
|
|
||||||
const locale = await RouteVariants.getLocale(props);
|
|
||||||
const { t } = await translation('metadata', locale);
|
|
||||||
return metadataModule.generate({
|
|
||||||
canonical: urlJoin(OFFICIAL_SITE, 'changelog'),
|
|
||||||
description: t('changelog.description', { appName: BRANDING_NAME }),
|
|
||||||
title: t('changelog.title'),
|
|
||||||
url: '/changelog',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const Page = async (props: DynamicLayoutProps) => {
|
|
||||||
const hideDocs = serverFeatureFlags().hideDocs;
|
|
||||||
if (hideDocs) return notFound();
|
|
||||||
|
|
||||||
const { isMobile, locale } = await RouteVariants.getVariantsFromProps(props);
|
|
||||||
const { t } = await translation('metadata', locale);
|
|
||||||
const changelogService = new ChangelogService();
|
|
||||||
const data = await changelogService.getChangelogIndex();
|
|
||||||
|
|
||||||
if (!data) return notFound();
|
|
||||||
|
|
||||||
const ld = ldModule.generate({
|
|
||||||
description: t('changelog.description', { appName: BRANDING_NAME }),
|
|
||||||
title: t('changelog.title', { appName: BRANDING_NAME }),
|
|
||||||
url: '/changelog',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<StructuredData ld={ld} />
|
|
||||||
<Flexbox gap={isMobile ? 16 : 48}>
|
|
||||||
{data?.map((item) => (
|
|
||||||
<Fragment key={item.id}>
|
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<GridLayout>
|
|
||||||
<Divider />
|
|
||||||
<Skeleton active paragraph={{ rows: 5 }} />
|
|
||||||
</GridLayout>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Post locale={locale} mobile={isMobile} {...item} />
|
|
||||||
</Suspense>
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
</Flexbox>
|
|
||||||
<GridLayout>
|
|
||||||
<Pagination />
|
|
||||||
</GridLayout>
|
|
||||||
<UpdateChangelogStatus currentId={data[0]?.id} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { memo, useEffect } from 'react';
|
|
||||||
import { useMediaQuery } from 'react-responsive';
|
|
||||||
import { MemoryRouter, Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import MainChatPage from './components/MainChatPage';
|
|
||||||
import SettingsPage from './components/SettingsPage';
|
|
||||||
|
|
||||||
// Get initial path from URL
|
|
||||||
const getInitialPath = () => {
|
|
||||||
if (typeof window === 'undefined') return '/';
|
|
||||||
const fullPath = window.location.pathname;
|
|
||||||
const searchParams = window.location.search;
|
|
||||||
const chatIndex = fullPath.indexOf('/chat');
|
|
||||||
|
|
||||||
if (chatIndex !== -1) {
|
|
||||||
const pathAfterChat = fullPath.slice(chatIndex + '/chat'.length) || '/';
|
|
||||||
return pathAfterChat + searchParams;
|
|
||||||
}
|
|
||||||
return '/';
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper component to sync URL with MemoryRouter
|
|
||||||
const UrlSynchronizer = () => {
|
|
||||||
const location = useLocation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
// Sync initial URL
|
|
||||||
useEffect(() => {
|
|
||||||
const fullPath = window.location.pathname;
|
|
||||||
const searchParams = window.location.search;
|
|
||||||
const chatIndex = fullPath.indexOf('/chat');
|
|
||||||
|
|
||||||
if (chatIndex !== -1) {
|
|
||||||
const pathAfterChat = fullPath.slice(chatIndex + '/chat'.length) || '/';
|
|
||||||
const targetPath = pathAfterChat + searchParams;
|
|
||||||
|
|
||||||
if (location.pathname + location.search !== targetPath) {
|
|
||||||
navigate(targetPath, { replace: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Update browser URL when location changes
|
|
||||||
useEffect(() => {
|
|
||||||
const normalizedPath = location.pathname === '/' ? '' : location.pathname;
|
|
||||||
const newUrl = `/chat${normalizedPath}${location.search}`;
|
|
||||||
if (window.location.pathname + window.location.search !== newUrl) {
|
|
||||||
window.history.replaceState({}, '', newUrl);
|
|
||||||
}
|
|
||||||
}, [location.pathname, location.search]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ChatRouter = memo(() => {
|
|
||||||
const mobile = useMediaQuery({ maxWidth: 768 });
|
|
||||||
const routes = (
|
|
||||||
<Routes>
|
|
||||||
<Route element={<MainChatPage mobile={true} />} path="/" />
|
|
||||||
<Route element={<SettingsPage mobile={true} />} path="/settings" />
|
|
||||||
<Route element={<Navigate replace to="/" />} path="*" />
|
|
||||||
</Routes>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MemoryRouter initialEntries={[getInitialPath()]} initialIndex={0}>
|
|
||||||
<UrlSynchronizer />
|
|
||||||
{mobile ? (
|
|
||||||
// Mobile Layout
|
|
||||||
routes
|
|
||||||
) : (
|
|
||||||
// Desktop Layout
|
|
||||||
<MainChatPage mobile={false} />
|
|
||||||
)}
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
ChatRouter.displayName = 'ChatRouter';
|
|
||||||
|
|
||||||
export default ChatRouter;
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { PropsWithChildren, memo } from 'react';
|
|
||||||
|
|
||||||
import Desktop from './Desktop';
|
|
||||||
import Mobile from './Mobile';
|
|
||||||
|
|
||||||
interface ChatLayoutProps extends PropsWithChildren {
|
|
||||||
mobile?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChatLayout = memo<ChatLayoutProps>(({ children, mobile }) => {
|
|
||||||
if (mobile) {
|
|
||||||
return <Mobile>{children}</Mobile>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Desktop>{children}</Desktop>;
|
|
||||||
});
|
|
||||||
|
|
||||||
ChatLayout.displayName = 'ChatLayout';
|
|
||||||
|
|
||||||
export default ChatLayout;
|
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
import { Suspense } from 'react';
|
|
||||||
import { Flexbox } from 'react-layout-kit';
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
import { isDesktop } from '@/const/version';
|
import { isDesktop } from '@/const/version';
|
||||||
import ProtocolUrlHandler from '@/features/ProtocolUrlHandler';
|
import ProtocolUrlHandler from '@/features/ProtocolUrlHandler';
|
||||||
|
|
||||||
import { LayoutProps } from '../type';
|
|
||||||
import RegisterHotkeys from './RegisterHotkeys';
|
import RegisterHotkeys from './RegisterHotkeys';
|
||||||
import SessionPanel from './SessionPanel';
|
import SessionPanel from './SessionPanel';
|
||||||
import Workspace from './Workspace';
|
import Workspace from './Workspace';
|
||||||
|
|
||||||
const Layout = ({ children }: LayoutProps) => {
|
const Layout = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Flexbox
|
<Flexbox
|
||||||
@@ -19,14 +18,14 @@ const Layout = ({ children }: LayoutProps) => {
|
|||||||
width={'100%'}
|
width={'100%'}
|
||||||
>
|
>
|
||||||
<SessionPanel />
|
<SessionPanel />
|
||||||
<Workspace>{children}</Workspace>
|
<Workspace>
|
||||||
|
<Outlet />
|
||||||
|
</Workspace>
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
{/* ↓ cloud slot ↓ */}
|
{/* ↓ cloud slot ↓ */}
|
||||||
|
|
||||||
{/* ↑ cloud slot ↑ */}
|
{/* ↑ cloud slot ↑ */}
|
||||||
<Suspense>
|
<RegisterHotkeys />
|
||||||
<RegisterHotkeys />
|
|
||||||
</Suspense>
|
|
||||||
{isDesktop && <ProtocolUrlHandler />}
|
{isDesktop && <ProtocolUrlHandler />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { withSuspense } from '@/components/withSuspense';
|
|||||||
import { useShowMobileWorkspace } from '@/hooks/useShowMobileWorkspace';
|
import { useShowMobileWorkspace } from '@/hooks/useShowMobileWorkspace';
|
||||||
|
|
||||||
import SessionPanelContent from '../components/SessionPanel';
|
import SessionPanelContent from '../components/SessionPanel';
|
||||||
import { LayoutProps } from './type';
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
const useStyles = createStyles(({ css, token }) => ({
|
const useStyles = createStyles(({ css, token }) => ({
|
||||||
main: css`
|
main: css`
|
||||||
@@ -18,7 +18,7 @@ const useStyles = createStyles(({ css, token }) => ({
|
|||||||
`,
|
`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const Layout = memo<LayoutProps>(({ children }) => {
|
const Layout = memo(( ) => {
|
||||||
const showMobileWorkspace = useShowMobileWorkspace();
|
const showMobileWorkspace = useShowMobileWorkspace();
|
||||||
const { styles } = useStyles();
|
const { styles } = useStyles();
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ const Layout = memo<LayoutProps>(({ children }) => {
|
|||||||
style={showMobileWorkspace ? undefined : { display: 'none' }}
|
style={showMobileWorkspace ? undefined : { display: 'none' }}
|
||||||
width="100%"
|
width="100%"
|
||||||
>
|
>
|
||||||
{children}
|
<Outlet />
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { memo } from 'react';
|
|
||||||
|
|
||||||
import TelemetryNotification from '../components/features/TelemetryNotification';
|
|
||||||
import PageTitle from '../features/PageTitle';
|
|
||||||
import WorkspaceLayout from './WorkspaceLayout';
|
|
||||||
|
|
||||||
interface MainChatPageProps {
|
|
||||||
mobile?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MainChatPage = memo<MainChatPageProps>(({ mobile }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PageTitle />
|
|
||||||
<WorkspaceLayout mobile={mobile} />
|
|
||||||
<TelemetryNotification mobile={mobile} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
MainChatPage.displayName = 'MainChatPage';
|
|
||||||
|
|
||||||
export default MainChatPage;
|
|
||||||
@@ -14,9 +14,6 @@ import ConversationArea from './ConversationArea';
|
|||||||
import PortalPanel from './PortalPanel';
|
import PortalPanel from './PortalPanel';
|
||||||
import TopicSidebar from './TopicSidebar';
|
import TopicSidebar from './TopicSidebar';
|
||||||
|
|
||||||
interface WorkspaceLayoutProps {
|
|
||||||
mobile?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DesktopWorkspace = memo(() => (
|
const DesktopWorkspace = memo(() => (
|
||||||
<>
|
<>
|
||||||
@@ -60,14 +57,4 @@ const MobileWorkspace = memo(() => (
|
|||||||
|
|
||||||
MobileWorkspace.displayName = 'MobileWorkspace';
|
MobileWorkspace.displayName = 'MobileWorkspace';
|
||||||
|
|
||||||
const WorkspaceLayout = memo<WorkspaceLayoutProps>(({ mobile }) => {
|
export { DesktopWorkspace, MobileWorkspace };
|
||||||
if (mobile) {
|
|
||||||
return <MobileWorkspace />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <DesktopWorkspace />;
|
|
||||||
});
|
|
||||||
|
|
||||||
WorkspaceLayout.displayName = 'WorkspaceLayout';
|
|
||||||
|
|
||||||
export default WorkspaceLayout;
|
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useQueryState } from 'nuqs';
|
|
||||||
import { memo, useLayoutEffect } from 'react';
|
import { memo, useLayoutEffect } from 'react';
|
||||||
import { createStoreUpdater } from 'zustand-utils';
|
import { createStoreUpdater } from 'zustand-utils';
|
||||||
|
|
||||||
|
import { useQueryState } from '@/hooks/useQueryParam';
|
||||||
import { useChatStore } from '@/store/chat';
|
import { useChatStore } from '@/store/chat';
|
||||||
|
|
||||||
// sync outside state to useChatStore
|
// sync outside state to useChatStore
|
||||||
@@ -34,7 +34,7 @@ const ChatHydration = memo(() => {
|
|||||||
unsubscribeTopic();
|
unsubscribeTopic();
|
||||||
unsubscribeThread();
|
unsubscribeThread();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [setTopic, setThread]); // ✅ 现在 setValue 是稳定的,可以安全地添加到依赖数组
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|||||||
+10
-12
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useSearchParams } from 'next/navigation';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { useChatStore } from '@/store/chat';
|
import { useChatStore } from '@/store/chat';
|
||||||
|
|
||||||
@@ -10,21 +10,19 @@ import { useSend } from '../useSend';
|
|||||||
const MessageFromUrl = () => {
|
const MessageFromUrl = () => {
|
||||||
const updateMessageInput = useChatStore((s) => s.updateMessageInput);
|
const updateMessageInput = useChatStore((s) => s.updateMessageInput);
|
||||||
const { send: sendMessage } = useSend();
|
const { send: sendMessage } = useSend();
|
||||||
const searchParams = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const message = searchParams.get('message');
|
const message = searchParams.get('message');
|
||||||
if (message) {
|
if (!message) return;
|
||||||
// Remove message from URL
|
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
|
||||||
params.delete('message');
|
|
||||||
const newUrl = `${window.location.pathname}?${params.toString()}`;
|
|
||||||
window.history.replaceState({}, '', newUrl);
|
|
||||||
|
|
||||||
updateMessageInput(message);
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
sendMessage();
|
params.delete('message');
|
||||||
}
|
setSearchParams(params, { replace: true });
|
||||||
}, [searchParams, updateMessageInput, sendMessage]);
|
|
||||||
|
updateMessageInput(message);
|
||||||
|
sendMessage();
|
||||||
|
}, [searchParams, setSearchParams, updateMessageInput, sendMessage]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
-1
@@ -10,7 +10,6 @@ import GroupChatInput from './GroupChat';
|
|||||||
|
|
||||||
const Desktop = memo((props: { targetMemberId?: string }) => {
|
const Desktop = memo((props: { targetMemberId?: string }) => {
|
||||||
const isGroupSession = useSessionStore(sessionSelectors.isCurrentSessionGroupSession);
|
const isGroupSession = useSessionStore(sessionSelectors.isCurrentSessionGroupSession);
|
||||||
|
|
||||||
return isGroupSession ? <GroupChatInput {...props} /> : <ClassicChatInput />;
|
return isGroupSession ? <GroupChatInput {...props} /> : <ClassicChatInput />;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useQueryState } from 'nuqs';
|
|
||||||
import { memo, useEffect, useLayoutEffect } from 'react';
|
import { memo, useEffect, useLayoutEffect } from 'react';
|
||||||
import { createStoreUpdater } from 'zustand-utils';
|
import { createStoreUpdater } from 'zustand-utils';
|
||||||
|
|
||||||
import { useFetchThreads } from '@/hooks/useFetchThreads';
|
import { useFetchThreads } from '@/hooks/useFetchThreads';
|
||||||
|
import { useQueryState } from '@/hooks/useQueryParam';
|
||||||
import { useChatStore } from '@/store/chat';
|
import { useChatStore } from '@/store/chat';
|
||||||
|
|
||||||
// sync outside state to useChatStore
|
// sync outside state to useChatStore
|
||||||
@@ -26,7 +26,7 @@ const ThreadHydration = memo(() => {
|
|||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
};
|
};
|
||||||
}, []);
|
}, [setThread]); // ✅ 现在 setValue 是稳定的,可以安全地添加到依赖数组
|
||||||
|
|
||||||
// should open portal automatically when portalThread is set
|
// should open portal automatically when portalThread is set
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
export { default } from '@/components/Error';
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import PageTitle from '@/components/PageTitle';
|
import PageTitle from '@/components/PageTitle';
|
||||||
import { withSuspense } from '@/components/withSuspense';
|
|
||||||
import { useChatStore } from '@/store/chat';
|
import { useChatStore } from '@/store/chat';
|
||||||
import { topicSelectors } from '@/store/chat/selectors';
|
import { topicSelectors } from '@/store/chat/selectors';
|
||||||
import { useSessionStore } from '@/store/session';
|
import { useSessionStore } from '@/store/session';
|
||||||
@@ -16,4 +15,4 @@ const Title = memo(() => {
|
|||||||
return <PageTitle title={[topicTitle, agentTitle].filter(Boolean).join(' · ')} />;
|
return <PageTitle title={[topicTitle, agentTitle].filter(Boolean).join(' · ')} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withSuspense(Title);
|
export default Title;
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
import { DesktopWorkspace, MobileWorkspace } from './components/WorkspaceLayout';
|
||||||
|
import TelemetryNotification from './components/features/TelemetryNotification';
|
||||||
|
import PageTitle from './features/PageTitle';
|
||||||
|
|
||||||
|
const MobileChatPage = memo(() => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageTitle />
|
||||||
|
<MobileWorkspace />
|
||||||
|
<TelemetryNotification mobile={true} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const DesktopChatPage = memo(() => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageTitle />
|
||||||
|
<DesktopWorkspace />
|
||||||
|
<TelemetryNotification mobile={false} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export { DesktopChatPage, MobileChatPage };
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import ServerLayout from '@/components/server/ServerLayout';
|
|
||||||
import Desktop from './_layout/Desktop';
|
|
||||||
import Mobile from './_layout/Mobile';
|
|
||||||
import { LayoutProps } from './_layout/type';
|
|
||||||
|
|
||||||
const Layout = ServerLayout<LayoutProps>({ Desktop, Mobile });
|
|
||||||
|
|
||||||
Layout.displayName = 'ChatLayout';
|
|
||||||
|
|
||||||
export default Layout;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import Loading from '@/components/Loading/BrandTextLoading';
|
|
||||||
|
|
||||||
export default () => <Loading />;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from '@/components/404';
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
import { BrandTextLoading } from '@/components/Loading';
|
|
||||||
|
|
||||||
const ChatRouter = dynamic(() => import('./ChatRouter'), {
|
|
||||||
loading: BrandTextLoading,
|
|
||||||
ssr: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default ChatRouter;
|
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useQueryState } from 'nuqs';
|
|
||||||
import { parseAsString } from 'nuqs/server';
|
|
||||||
import { memo, useEffect } from 'react';
|
import { memo, useEffect } from 'react';
|
||||||
import { createStoreUpdater } from 'zustand-utils';
|
import { createStoreUpdater } from 'zustand-utils';
|
||||||
|
|
||||||
|
import { parseAsString, useQueryParam } from '@/hooks/useQueryParam';
|
||||||
import { useAgentStore } from '@/store/agent';
|
import { useAgentStore } from '@/store/agent';
|
||||||
import { useChatStore } from '@/store/chat';
|
import { useChatStore } from '@/store/chat';
|
||||||
import { useSessionStore } from '@/store/session';
|
import { useSessionStore } from '@/store/session';
|
||||||
|
|
||||||
|
const THROTTLE_DELAY = 50;
|
||||||
|
|
||||||
// sync outside state to useSessionStore
|
// sync outside state to useSessionStore
|
||||||
const SessionHydration = memo(() => {
|
const SessionHydration = memo(() => {
|
||||||
const useStoreUpdater = createStoreUpdater(useSessionStore);
|
const useStoreUpdater = createStoreUpdater(useSessionStore);
|
||||||
@@ -17,10 +18,11 @@ const SessionHydration = memo(() => {
|
|||||||
const [switchTopic] = useChatStore((s) => [s.switchTopic]);
|
const [switchTopic] = useChatStore((s) => [s.switchTopic]);
|
||||||
|
|
||||||
// two-way bindings the url and session store
|
// two-way bindings the url and session store
|
||||||
const [session, setSession] = useQueryState(
|
const [session, setSession] = useQueryParam('session', parseAsString.withDefault('inbox'), {
|
||||||
'session',
|
history: 'replace',
|
||||||
parseAsString.withDefault('inbox').withOptions({ history: 'replace', throttleMs: 50 }),
|
throttleMs: THROTTLE_DELAY,
|
||||||
);
|
});
|
||||||
|
|
||||||
useStoreUpdater('activeId', session);
|
useStoreUpdater('activeId', session);
|
||||||
useAgentStoreUpdater('activeId', session);
|
useAgentStoreUpdater('activeId', session);
|
||||||
useChatStoreUpdater('activeId', session);
|
useChatStoreUpdater('activeId', session);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Link from 'next/link';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { DEFAULT_INBOX_AVATAR } from '@/const/meta';
|
import { DEFAULT_INBOX_AVATAR } from '@/const/meta';
|
||||||
import { INBOX_SESSION_ID } from '@/const/session';
|
import { INBOX_SESSION_ID } from '@/const/session';
|
||||||
@@ -24,15 +24,12 @@ const Inbox = memo(() => {
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
aria-label={t('inbox.title')}
|
aria-label={t('inbox.title')}
|
||||||
href={SESSION_CHAT_URL(INBOX_SESSION_ID, mobile)}
|
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (activeId === INBOX_SESSION_ID && !mobile) {
|
if (activeId === INBOX_SESSION_ID && !mobile) {
|
||||||
// If user tap the inbox again, open a new topic.
|
// If user tap the inbox again, open a new topic.
|
||||||
// Only for desktop.
|
// Only for desktop.
|
||||||
const inboxMessages = chatSelectors.inboxActiveTopicMessages(getChatStoreState());
|
const inboxMessages = chatSelectors.inboxActiveTopicMessages(getChatStoreState());
|
||||||
|
|
||||||
if (inboxMessages.length > 0) {
|
if (inboxMessages.length > 0) {
|
||||||
await openNewTopicOrSaveTopic();
|
await openNewTopicOrSaveTopic();
|
||||||
}
|
}
|
||||||
@@ -40,6 +37,7 @@ const Inbox = memo(() => {
|
|||||||
switchSession(INBOX_SESSION_ID);
|
switchSession(INBOX_SESSION_ID);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
to={SESSION_CHAT_URL(INBOX_SESSION_ID, mobile)}
|
||||||
>
|
>
|
||||||
<ListItem
|
<ListItem
|
||||||
active={activeId === INBOX_SESSION_ID}
|
active={activeId === INBOX_SESSION_ID}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import LazyLoad from 'react-lazy-load';
|
|||||||
|
|
||||||
import { SESSION_CHAT_URL } from '@/const/url';
|
import { SESSION_CHAT_URL } from '@/const/url';
|
||||||
import { useSwitchSession } from '@/hooks/useSwitchSession';
|
import { useSwitchSession } from '@/hooks/useSwitchSession';
|
||||||
|
import { useSessionStore, getSessionStoreState } from '@/store/session';
|
||||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||||
import { getSessionStoreState, useSessionStore } from '@/store/session';
|
|
||||||
import { sessionGroupSelectors, sessionSelectors } from '@/store/session/selectors';
|
import { sessionGroupSelectors, sessionSelectors } from '@/store/session/selectors';
|
||||||
import { getUserStoreState } from '@/store/user';
|
import { getUserStoreState } from '@/store/user';
|
||||||
import { userProfileSelectors } from '@/store/user/selectors';
|
import { userProfileSelectors } from '@/store/user/selectors';
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import { ActionIcon } from '@lobehub/ui';
|
import { ActionIcon } from '@lobehub/ui';
|
||||||
import { ChatHeader } from '@lobehub/ui/mobile';
|
import { ChatHeader } from '@lobehub/ui/mobile';
|
||||||
import { MessageSquarePlus } from 'lucide-react';
|
import { MessageSquarePlus } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Flexbox } from 'react-layout-kit';
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
import { ProductLogo } from '@/components/Branding';
|
import { ProductLogo } from '@/components/Branding';
|
||||||
@@ -16,14 +16,14 @@ import { mobileHeaderSticky } from '@/styles/mobileHeader';
|
|||||||
|
|
||||||
const Header = memo(() => {
|
const Header = memo(() => {
|
||||||
const [createSession] = useSessionStore((s) => [s.createSession]);
|
const [createSession] = useSessionStore((s) => [s.createSession]);
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const { showCreateSession } = useServerConfigStore(featureFlagsSelectors);
|
const { showCreateSession } = useServerConfigStore(featureFlagsSelectors);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatHeader
|
<ChatHeader
|
||||||
left={
|
left={
|
||||||
<Flexbox align={'center'} gap={8} horizontal style={{ marginLeft: 8 }}>
|
<Flexbox align={'center'} gap={8} horizontal style={{ marginLeft: 8 }}>
|
||||||
<UserAvatar onClick={() => router.push('/me')} size={32} />
|
<UserAvatar onClick={() => navigate('/me')} size={32} />
|
||||||
<ProductLogo type={'text'} />
|
<ProductLogo type={'text'} />
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChatHeader, ChatHeaderTitle } from '@lobehub/ui/chat';
|
import { ChatHeader, ChatHeaderTitle } from '@lobehub/ui/chat';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { pathString } from '@/utils/url';
|
import { pathString } from '@/utils/url';
|
||||||
|
|
||||||
@@ -11,12 +11,12 @@ import HeaderContent from '../../features/HeaderContent';
|
|||||||
|
|
||||||
const Header = memo(() => {
|
const Header = memo(() => {
|
||||||
const { t } = useTranslation('setting');
|
const { t } = useTranslation('setting');
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatHeader
|
<ChatHeader
|
||||||
left={<ChatHeaderTitle title={t('header.session')} />}
|
left={<ChatHeaderTitle title={t('header.session')} />}
|
||||||
onBackClick={() => router.push(pathString('/chat', { search: location.search }))}
|
onBackClick={() => navigate(pathString('/chat', { search: location.search }))}
|
||||||
right={<HeaderContent />}
|
right={<HeaderContent />}
|
||||||
showBackButton
|
showBackButton
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,20 +3,20 @@
|
|||||||
import { ChatHeader } from '@lobehub/ui/mobile';
|
import { ChatHeader } from '@lobehub/ui/mobile';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { useQueryRoute } from '@/hooks/useQueryRoute';
|
|
||||||
import { mobileHeaderSticky } from '@/styles/mobileHeader';
|
import { mobileHeaderSticky } from '@/styles/mobileHeader';
|
||||||
|
|
||||||
import HeaderContent from '../../features/HeaderContent';
|
import HeaderContent from '../../features/HeaderContent';
|
||||||
|
|
||||||
const Header = memo(() => {
|
const Header = memo(() => {
|
||||||
const { t } = useTranslation('setting');
|
const { t } = useTranslation('setting');
|
||||||
const router = useQueryRoute();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatHeader
|
<ChatHeader
|
||||||
center={<ChatHeader.Title title={t('header.session')} />}
|
center={<ChatHeader.Title title={t('header.session')} />}
|
||||||
onBackClick={() => router.push('/chat')}
|
onBackClick={() => navigate(-1)}
|
||||||
right={<HeaderContent />}
|
right={<HeaderContent />}
|
||||||
showBackButton
|
showBackButton
|
||||||
style={mobileHeaderSticky}
|
style={mobileHeaderSticky}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
export { default } from '@/components/Error';
|
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
import { CheckCircleOutlined } from '@ant-design/icons';
|
import { CheckCircleOutlined } from '@ant-design/icons';
|
||||||
import { Button, Modal, Space } from 'antd';
|
import { Button, Modal, Space } from 'antd';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
interface PublishResultModalProps {
|
interface PublishResultModalProps {
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
@@ -13,13 +13,13 @@ interface PublishResultModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PublishResultModal = memo<PublishResultModalProps>(({ identifier, onCancel, open }) => {
|
const PublishResultModal = memo<PublishResultModalProps>(({ identifier, onCancel, open }) => {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation('setting');
|
const { t } = useTranslation('setting');
|
||||||
const { t: tCommon } = useTranslation('common');
|
const { t: tCommon } = useTranslation('common');
|
||||||
|
|
||||||
const handleGoToMarket = () => {
|
const handleGoToMarket = () => {
|
||||||
if (identifier) {
|
if (identifier) {
|
||||||
router.push(`/discover/assistant/${identifier}`);
|
navigate(`/discover/assistant/${identifier}`);
|
||||||
}
|
}
|
||||||
onCancel();
|
onCancel();
|
||||||
};
|
};
|
||||||
|
|||||||
+6
-35
@@ -8,12 +8,9 @@ import { useTranslation } from 'react-i18next';
|
|||||||
|
|
||||||
import MobileContentLayout from '@/components/server/MobileNavLayout';
|
import MobileContentLayout from '@/components/server/MobileNavLayout';
|
||||||
import PageTitle from '@/components/PageTitle';
|
import PageTitle from '@/components/PageTitle';
|
||||||
import SafeSpacing from '@/components/SafeSpacing';
|
|
||||||
import { HEADER_HEIGHT } from '@/const/layoutTokens';
|
|
||||||
import { useCategory } from '@/features/AgentSetting/AgentCategory/useCategory';
|
import { useCategory } from '@/features/AgentSetting/AgentCategory/useCategory';
|
||||||
import AgentSettings from '@/features/AgentSetting/AgentSettings';
|
import AgentSettings from '@/features/AgentSetting/AgentSettings';
|
||||||
import Footer from '@/features/Setting/Footer';
|
import Footer from '@/features/Setting/Footer';
|
||||||
import SettingContainer from '@/features/Setting/SettingContainer';
|
|
||||||
import { useInitAgentConfig } from '@/hooks/useInitAgentConfig';
|
import { useInitAgentConfig } from '@/hooks/useInitAgentConfig';
|
||||||
import { useAgentStore } from '@/store/agent';
|
import { useAgentStore } from '@/store/agent';
|
||||||
import { agentSelectors } from '@/store/agent/selectors';
|
import { agentSelectors } from '@/store/agent/selectors';
|
||||||
@@ -21,14 +18,9 @@ import { ChatSettingsTabs } from '@/store/global/initialState';
|
|||||||
import { useSessionStore } from '@/store/session';
|
import { useSessionStore } from '@/store/session';
|
||||||
import { sessionMetaSelectors } from '@/store/session/selectors';
|
import { sessionMetaSelectors } from '@/store/session/selectors';
|
||||||
|
|
||||||
import DesktopHeader from '../settings/_layout/Desktop/Header';
|
import MobileHeader from './_layout/Mobile/Header';
|
||||||
import MobileHeader from '../settings/_layout/Mobile/Header';
|
|
||||||
|
|
||||||
interface SettingsPageProps {
|
export default memo(() => {
|
||||||
mobile?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SettingsPage = memo<SettingsPageProps>(({ mobile = false }) => {
|
|
||||||
const { t } = useTranslation('setting');
|
const { t } = useTranslation('setting');
|
||||||
const [tab, setTab] = useState(ChatSettingsTabs.Prompt);
|
const [tab, setTab] = useState(ChatSettingsTabs.Prompt);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -46,8 +38,8 @@ const SettingsPage = memo<SettingsPageProps>(({ mobile = false }) => {
|
|||||||
|
|
||||||
const { isLoading } = useInitAgentConfig();
|
const { isLoading } = useInitAgentConfig();
|
||||||
|
|
||||||
const content = (
|
return (
|
||||||
<>
|
<MobileContentLayout header={<MobileHeader />}>
|
||||||
<PageTitle title={t('header.sessionWithName', { name: title })} />
|
<PageTitle title={t('header.sessionWithName', { name: title })} />
|
||||||
<Tabs
|
<Tabs
|
||||||
activeKey={tab}
|
activeKey={tab}
|
||||||
@@ -67,28 +59,7 @@ const SettingsPage = memo<SettingsPageProps>(({ mobile = false }) => {
|
|||||||
onMetaChange={updateAgentMeta}
|
onMetaChange={updateAgentMeta}
|
||||||
tab={tab}
|
tab={tab}
|
||||||
/>
|
/>
|
||||||
</>
|
<Footer />
|
||||||
);
|
</MobileContentLayout>
|
||||||
|
|
||||||
if (mobile) {
|
|
||||||
return (
|
|
||||||
<MobileContentLayout header={<MobileHeader />}>
|
|
||||||
{content}
|
|
||||||
<Footer />
|
|
||||||
</MobileContentLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DesktopHeader />
|
|
||||||
<SettingContainer addonAfter={<Footer />} addonBefore={<SafeSpacing height={HEADER_HEIGHT} />}>
|
|
||||||
{content}
|
|
||||||
</SettingContainer>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
SettingsPage.displayName = 'SettingsPage';
|
|
||||||
|
|
||||||
export default SettingsPage;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import SkeletonLoading from '@/components/Loading/SkeletonLoading';
|
|
||||||
|
|
||||||
export default () => <SkeletonLoading paragraph={{ rows: 8 }} />;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default } from '@/components/404';
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React, { memo } from 'react';
|
||||||
|
import { Link as ReactRouterLink, LinkProps as ReactRouterLinkProps } from 'react-router-dom';
|
||||||
|
|
||||||
|
interface LinkProps extends Omit<ReactRouterLinkProps, 'to'> {
|
||||||
|
children?: React.ReactNode;
|
||||||
|
href?: string;
|
||||||
|
to?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link component for React Router
|
||||||
|
* Provides a Next.js-like API (href prop) while using React Router internally
|
||||||
|
*/
|
||||||
|
const Link = memo<LinkProps>(({ href, to, ...props }) => {
|
||||||
|
const linkTo = href || to || '/';
|
||||||
|
return <ReactRouterLink {...props} to={linkTo} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
Link.displayName = 'Link';
|
||||||
|
|
||||||
|
export default Link;
|
||||||
+15
-7
@@ -1,12 +1,19 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
'use client';
|
||||||
|
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
import { Flexbox } from 'react-layout-kit';
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
import { SCROLL_PARENT_ID } from '@/app/[variants]/(main)/discover/features/const';
|
import { SCROLL_PARENT_ID } from '../../../features/const';
|
||||||
import Footer from '@/features/Setting/Footer';
|
import Footer from '@/features/Setting/Footer';
|
||||||
|
|
||||||
const MAX_WIDTH = 1440;
|
const MAX_WIDTH = 1440;
|
||||||
|
|
||||||
const Layout = ({ children }: PropsWithChildren) => {
|
/**
|
||||||
|
* Desktop Discover Detail Layout
|
||||||
|
* Layout for detail pages (assistant, model, provider, mcp details)
|
||||||
|
*/
|
||||||
|
const DesktopDiscoverDetailLayout = memo(() => {
|
||||||
return (
|
return (
|
||||||
<Flexbox
|
<Flexbox
|
||||||
align={'center'}
|
align={'center'}
|
||||||
@@ -17,14 +24,15 @@ const Layout = ({ children }: PropsWithChildren) => {
|
|||||||
width={'100%'}
|
width={'100%'}
|
||||||
>
|
>
|
||||||
<Flexbox gap={24} style={{ maxWidth: MAX_WIDTH, minHeight: '100%' }} width={'100%'}>
|
<Flexbox gap={24} style={{ maxWidth: MAX_WIDTH, minHeight: '100%' }} width={'100%'}>
|
||||||
{children}
|
<Outlet />
|
||||||
<div />
|
<div />
|
||||||
<Footer />
|
<Footer />
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
</Flexbox>
|
</Flexbox>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
Layout.displayName = 'DesktopDiscoverDetailLayout';
|
DesktopDiscoverDetailLayout.displayName = 'DesktopDiscoverDetailLayout';
|
||||||
|
|
||||||
export default Layout;
|
|
||||||
|
export default DesktopDiscoverDetailLayout;
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { PropsWithChildren, memo } from 'react';
|
|
||||||
|
|
||||||
import Desktop from './Desktop';
|
|
||||||
import Mobile from './Mobile';
|
|
||||||
|
|
||||||
interface DetailLayoutProps extends PropsWithChildren {
|
|
||||||
mobile?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DetailLayout = memo<DetailLayoutProps>(({ children, mobile }) => {
|
|
||||||
if (mobile) {
|
|
||||||
return <Mobile>{children}</Mobile>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Desktop>{children}</Desktop>;
|
|
||||||
});
|
|
||||||
|
|
||||||
DetailLayout.displayName = 'DetailLayout';
|
|
||||||
|
|
||||||
export default DetailLayout;
|
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
|
||||||
|
|
||||||
import { SCROLL_PARENT_ID } from '@/app/[variants]/(main)/discover/features/const';
|
import { SCROLL_PARENT_ID } from '@/app/[variants]/(main)/discover/features/const';
|
||||||
import MobileContentLayout from '@/components/server/MobileNavLayout';
|
import MobileContentLayout from '@/components/server/MobileNavLayout';
|
||||||
import Footer from '@/features/Setting/Footer';
|
import Footer from '@/features/Setting/Footer';
|
||||||
|
|
||||||
import Header from './Header';
|
import Header from './Header';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
const Layout = ({ children }: PropsWithChildren) => {
|
const Layout = () => {
|
||||||
return (
|
return (
|
||||||
<MobileContentLayout gap={16} header={<Header />} id={SCROLL_PARENT_ID} padding={16}>
|
<MobileContentLayout gap={16} header={<Header />} id={SCROLL_PARENT_ID} padding={16}>
|
||||||
{children}
|
<Outlet />
|
||||||
<div />
|
<div />
|
||||||
<Footer />
|
<Footer />
|
||||||
</MobileContentLayout>
|
</MobileContentLayout>
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { notFound } from 'next/navigation';
|
|
||||||
import { memo } from 'react';
|
|
||||||
import { Flexbox } from 'react-layout-kit';
|
|
||||||
|
|
||||||
import { withSuspense } from '@/components/withSuspense';
|
|
||||||
import { useQuery } from '@/hooks/useQuery';
|
|
||||||
import { useDiscoverStore } from '@/store/discover';
|
|
||||||
import { AssistantMarketSource } from '@/types/discover';
|
|
||||||
|
|
||||||
import { TocProvider } from '../../features/Toc/useToc';
|
|
||||||
import { DetailProvider } from './features/DetailProvider';
|
|
||||||
import Details from './features/Details';
|
|
||||||
import Header from './features/Header';
|
|
||||||
import StatusPage from './features/StatusPage';
|
|
||||||
import Loading from './loading';
|
|
||||||
|
|
||||||
interface ClientProps {
|
|
||||||
identifier: string;
|
|
||||||
mobile?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Client = memo<ClientProps>(({ identifier, mobile }) => {
|
|
||||||
const { version, source } = useQuery() as { source?: AssistantMarketSource; version?: string };
|
|
||||||
const marketSource = source as AssistantMarketSource | undefined;
|
|
||||||
const useAssistantDetail = useDiscoverStore((s) => s.useAssistantDetail);
|
|
||||||
const { data, isLoading } = useAssistantDetail({ identifier, source: marketSource, version });
|
|
||||||
|
|
||||||
if (isLoading) return <Loading />;
|
|
||||||
if (!data) return notFound();
|
|
||||||
|
|
||||||
// 检查助手状态
|
|
||||||
const status = (data as any)?.status;
|
|
||||||
if (status === 'unpublished' || status === 'archived' || status === 'deprecated') {
|
|
||||||
return <StatusPage status={status} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TocProvider>
|
|
||||||
<DetailProvider config={data}>
|
|
||||||
<Flexbox gap={16}>
|
|
||||||
<Header mobile={mobile} />
|
|
||||||
<Details mobile={mobile} />
|
|
||||||
</Flexbox>
|
|
||||||
</DetailProvider>
|
|
||||||
</TocProvider>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default withSuspense(Client);
|
|
||||||
+1
-1
@@ -2,7 +2,7 @@ import { Tag } from '@lobehub/ui';
|
|||||||
import { ReactNode, memo } from 'react';
|
import { ReactNode, memo } from 'react';
|
||||||
import { Flexbox } from 'react-layout-kit';
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
import Title from '../../../../../../features/Title';
|
import Title from '../../../../../features/Title';
|
||||||
|
|
||||||
interface BlockProps {
|
interface BlockProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
+1
-1
@@ -10,7 +10,7 @@ import { DEFAULT_USER_AVATAR_URL } from '@/const/meta';
|
|||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
import { authSelectors, userProfileSelectors } from '@/store/user/selectors';
|
import { authSelectors, userProfileSelectors } from '@/store/user/selectors';
|
||||||
|
|
||||||
import Title from '../../../../../../features/Title';
|
import Title from '../../../../../features/Title';
|
||||||
import { useDetailContext } from '../../DetailProvider';
|
import { useDetailContext } from '../../DetailProvider';
|
||||||
|
|
||||||
const Overview = memo(() => {
|
const Overview = memo(() => {
|
||||||
+2
-2
@@ -6,8 +6,8 @@ import { Flexbox } from 'react-layout-kit';
|
|||||||
import { useQuery } from '@/hooks/useQuery';
|
import { useQuery } from '@/hooks/useQuery';
|
||||||
import { AssistantMarketSource } from '@/types/discover';
|
import { AssistantMarketSource } from '@/types/discover';
|
||||||
|
|
||||||
import McpList from '../../../../../../(list)/assistant/features/List';
|
import McpList from '../../../../../(list)/assistant/features/List';
|
||||||
import Title from '../../../../../../features/Title';
|
import Title from '../../../../../features/Title';
|
||||||
import { useDetailContext } from '../../DetailProvider';
|
import { useDetailContext } from '../../DetailProvider';
|
||||||
|
|
||||||
const Related = memo(() => {
|
const Related = memo(() => {
|
||||||
+3
-3
@@ -5,9 +5,9 @@ import { memo } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Flexbox } from 'react-layout-kit';
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
import TokenTag from '../../../../../../(list)/assistant/features/List/TokenTag';
|
import TokenTag from '../../../../../(list)/assistant/features/List/TokenTag';
|
||||||
import Title from '../../../../../../features/Title';
|
import Title from '../../../../../features/Title';
|
||||||
import MarkdownRender from '../../../../../features/MakedownRender';
|
import MarkdownRender from '../../../../features/MakedownRender';
|
||||||
import { useDetailContext } from '../../DetailProvider';
|
import { useDetailContext } from '../../DetailProvider';
|
||||||
import TagList from './TagList';
|
import TagList from './TagList';
|
||||||
|
|
||||||
+4
-4
@@ -1,19 +1,19 @@
|
|||||||
import { Block, Icon, Tag } from '@lobehub/ui';
|
import { Block, Icon, Tag } from '@lobehub/ui';
|
||||||
import { useTheme } from 'antd-style';
|
import { useTheme } from 'antd-style';
|
||||||
import { CheckIcon, MinusIcon } from 'lucide-react';
|
import { CheckIcon, MinusIcon } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import qs from 'query-string';
|
import qs from 'query-string';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Flexbox } from 'react-layout-kit';
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import Link from '@/app/[variants]/(main)/components/Link';
|
||||||
|
import { usePathname } from '@/app/[variants]/(main)/hooks/usePathname';
|
||||||
|
import { useQuery } from '@/app/[variants]/(main)/hooks/useQuery';
|
||||||
import InlineTable from '@/components/InlineTable';
|
import InlineTable from '@/components/InlineTable';
|
||||||
import PublishedTime from '@/components/PublishedTime';
|
import PublishedTime from '@/components/PublishedTime';
|
||||||
import { useQuery } from '@/hooks/useQuery';
|
|
||||||
import { AssistantMarketSource, AssistantNavKey } from '@/types/discover';
|
import { AssistantMarketSource, AssistantNavKey } from '@/types/discover';
|
||||||
|
|
||||||
import Title from '../../../../../../features/Title';
|
import Title from '../../../../../features/Title';
|
||||||
import { useDetailContext } from '../../DetailProvider';
|
import { useDetailContext } from '../../DetailProvider';
|
||||||
|
|
||||||
const Versions = memo(() => {
|
const Versions = memo(() => {
|
||||||
+1
-1
@@ -1,10 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useResponsive } from 'antd-style';
|
import { useResponsive } from 'antd-style';
|
||||||
import { useQueryState } from 'nuqs';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Flexbox } from 'react-layout-kit';
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import { useQueryState } from '@/hooks/useQueryParam';
|
||||||
import { AssistantNavKey } from '@/types/discover';
|
import { AssistantNavKey } from '@/types/discover';
|
||||||
|
|
||||||
import Sidebar from '../Sidebar';
|
import Sidebar from '../Sidebar';
|
||||||
+2
-2
@@ -13,8 +13,8 @@ import urlJoin from 'url-join';
|
|||||||
|
|
||||||
import { formatIntergerNumber } from '@/utils/format';
|
import { formatIntergerNumber } from '@/utils/format';
|
||||||
|
|
||||||
import { useCategory } from '../../../../(list)/assistant/features/Category/useCategory';
|
import { useCategory } from '../../../(list)/assistant/features/Category/useCategory';
|
||||||
import PublishedTime from '../../../../../../../../components/PublishedTime';
|
import PublishedTime from '../../../../../../../components/PublishedTime';
|
||||||
import { useDetailContext } from './DetailProvider';
|
import { useDetailContext } from './DetailProvider';
|
||||||
|
|
||||||
const useStyles = createStyles(({ css, token }) => {
|
const useStyles = createStyles(({ css, token }) => {
|
||||||
+3
-3
@@ -4,9 +4,9 @@ import { Icon } from '@lobehub/ui';
|
|||||||
import { App, Dropdown } from 'antd';
|
import { App, Dropdown } from 'antd';
|
||||||
import { createStyles } from 'antd-style';
|
import { createStyles } from 'antd-style';
|
||||||
import { ChevronDownIcon } from 'lucide-react';
|
import { ChevronDownIcon } from 'lucide-react';
|
||||||
import { useRouter } from 'nextjs-toploader/app';
|
|
||||||
import { memo, useState } from 'react';
|
import { memo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { SESSION_CHAT_URL } from '@/const/url';
|
import { SESSION_CHAT_URL } from '@/const/url';
|
||||||
import { useSessionStore } from '@/store/session';
|
import { useSessionStore } from '@/store/session';
|
||||||
@@ -70,7 +70,7 @@ const AddAgent = memo<{ mobile?: boolean }>(({ mobile }) => {
|
|||||||
const createSession = useSessionStore((s) => s.createSession);
|
const createSession = useSessionStore((s) => s.createSession);
|
||||||
const sessions = useSessionStore((s) => s.sessions);
|
const sessions = useSessionStore((s) => s.sessions);
|
||||||
const { message, modal } = App.useApp();
|
const { message, modal } = App.useApp();
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation('discover');
|
const { t } = useTranslation('discover');
|
||||||
|
|
||||||
const meta = {
|
const meta = {
|
||||||
@@ -114,7 +114,7 @@ const AddAgent = memo<{ mobile?: boolean }>(({ mobile }) => {
|
|||||||
try {
|
try {
|
||||||
const session = await createSessionWithMarketIdentifier(true);
|
const session = await createSessionWithMarketIdentifier(true);
|
||||||
message.success(t('assistants.addAgentSuccess'));
|
message.success(t('assistants.addAgentSuccess'));
|
||||||
router.push(SESSION_CHAT_URL(session, mobile));
|
navigate(SESSION_CHAT_URL(session, mobile));
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
+1
-1
@@ -6,7 +6,7 @@ import urlJoin from 'url-join';
|
|||||||
|
|
||||||
import { OFFICIAL_URL } from '@/const/url';
|
import { OFFICIAL_URL } from '@/const/url';
|
||||||
|
|
||||||
import ShareButton from '../../../../../features/ShareButton';
|
import ShareButton from '../../../../features/ShareButton';
|
||||||
import { useDetailContext } from '../../DetailProvider';
|
import { useDetailContext } from '../../DetailProvider';
|
||||||
import AddAgent from './AddAgent';
|
import AddAgent from './AddAgent';
|
||||||
|
|
||||||
+1
-1
@@ -5,7 +5,7 @@ import { Flexbox } from 'react-layout-kit';
|
|||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import urlJoin from 'url-join';
|
import urlJoin from 'url-join';
|
||||||
|
|
||||||
import Title from '../../../../../../features/Title';
|
import Title from '../../../../../features/Title';
|
||||||
import { useQuery } from '@/hooks/useQuery';
|
import { useQuery } from '@/hooks/useQuery';
|
||||||
import { AssistantMarketSource } from '@/types/discover';
|
import { AssistantMarketSource } from '@/types/discover';
|
||||||
import { useDetailContext } from '../../DetailProvider';
|
import { useDetailContext } from '../../DetailProvider';
|
||||||
+2
-2
@@ -9,8 +9,8 @@ import { useToc } from '@/app/[variants]/(main)/discover/(detail)/features/Toc/u
|
|||||||
import { useQuery } from '@/hooks/useQuery';
|
import { useQuery } from '@/hooks/useQuery';
|
||||||
import { AssistantNavKey } from '@/types/discover';
|
import { AssistantNavKey } from '@/types/discover';
|
||||||
|
|
||||||
import Title from '../../../../../../features/Title';
|
import Title from '../../../../../features/Title';
|
||||||
import Toc from '../../../../../features/Toc';
|
import Toc from '../../../../features/Toc';
|
||||||
|
|
||||||
const TocList = memo(() => {
|
const TocList = memo(() => {
|
||||||
const { t } = useTranslation('discover');
|
const { t } = useTranslation('discover');
|
||||||
+3
-3
@@ -6,20 +6,20 @@ import {
|
|||||||
FolderOpenOutlined,
|
FolderOpenOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Button, Result } from 'antd';
|
import { Button, Result } from 'antd';
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
interface StatusPageProps {
|
interface StatusPageProps {
|
||||||
status: 'unpublished' | 'archived' | 'deprecated';
|
status: 'unpublished' | 'archived' | 'deprecated';
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusPage = memo<StatusPageProps>(({ status }) => {
|
const StatusPage = memo<StatusPageProps>(({ status }) => {
|
||||||
const router = useRouter();
|
const navigate = useNavigate();
|
||||||
const { t } = useTranslation('discover');
|
const { t } = useTranslation('discover');
|
||||||
|
|
||||||
const handleBackToMarket = () => {
|
const handleBackToMarket = () => {
|
||||||
router.push('/discover/assistant');
|
navigate('/discover/assistant');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 审核中状态
|
// 审核中状态
|
||||||
+18
-11
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Flexbox } from 'react-layout-kit';
|
import { Flexbox } from 'react-layout-kit';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useLoaderData } from 'react-router-dom';
|
||||||
|
|
||||||
import { withSuspense } from '@/components/withSuspense';
|
import type { SlugParams } from '@/app/[variants]/loaders/routeParams';
|
||||||
import { useQuery } from '@/hooks/useQuery';
|
import { useQuery } from '@/hooks/useQuery';
|
||||||
import { useDiscoverStore } from '@/store/discover';
|
import { useDiscoverStore } from '@/store/discover';
|
||||||
import { AssistantMarketSource, DiscoverTab } from '@/types/discover';
|
import { AssistantMarketSource, DiscoverTab } from '@/types/discover';
|
||||||
@@ -12,20 +12,19 @@ import { AssistantMarketSource, DiscoverTab } from '@/types/discover';
|
|||||||
import NotFound from '../components/NotFound';
|
import NotFound from '../components/NotFound';
|
||||||
import Breadcrumb from '../features/Breadcrumb';
|
import Breadcrumb from '../features/Breadcrumb';
|
||||||
import { TocProvider } from '../features/Toc/useToc';
|
import { TocProvider } from '../features/Toc/useToc';
|
||||||
import { DetailProvider } from './[...slugs]/features/DetailProvider';
|
import { DetailProvider } from './features/DetailProvider';
|
||||||
import Details from './[...slugs]/features/Details';
|
import Details from './features/Details';
|
||||||
import Header from './[...slugs]/features/Header';
|
import Header from './features/Header';
|
||||||
import StatusPage from './[...slugs]/features/StatusPage';
|
import StatusPage from './features/StatusPage';
|
||||||
import Loading from './[...slugs]/loading';
|
import Loading from './loading';
|
||||||
|
|
||||||
interface AssistantDetailPageProps {
|
interface AssistantDetailPageProps {
|
||||||
mobile?: boolean;
|
mobile?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AssistantDetailPage = memo<AssistantDetailPageProps>(({ mobile }) => {
|
const AssistantDetailPage = memo<AssistantDetailPageProps>(({ mobile }) => {
|
||||||
const params = useParams();
|
const { slug } = useLoaderData() as SlugParams;
|
||||||
const slugs = params['*']?.split('/') || [];
|
const identifier = decodeURIComponent(slug);
|
||||||
const identifier = decodeURIComponent(slugs.join('/'));
|
|
||||||
const { version, source } = useQuery() as { source?: AssistantMarketSource; version?: string };
|
const { version, source } = useQuery() as { source?: AssistantMarketSource; version?: string };
|
||||||
|
|
||||||
const useAssistantDetail = useDiscoverStore((s) => s.useAssistantDetail);
|
const useAssistantDetail = useDiscoverStore((s) => s.useAssistantDetail);
|
||||||
@@ -53,4 +52,12 @@ const AssistantDetailPage = memo<AssistantDetailPageProps>(({ mobile }) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default withSuspense(AssistantDetailPage);
|
const DesktopDiscoverAssistantDetailPage = memo<{ mobile?: boolean }>(() => {
|
||||||
|
return <AssistantDetailPage mobile={false} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
const MobileDiscoverAssistantDetailPage = memo<{ mobile?: boolean }>(() => {
|
||||||
|
return <AssistantDetailPage mobile={true} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
export { DesktopDiscoverAssistantDetailPage, MobileDiscoverAssistantDetailPage };
|
||||||
@@ -3,12 +3,12 @@
|
|||||||
import { CopyButton } from '@lobehub/ui';
|
import { CopyButton } from '@lobehub/ui';
|
||||||
import { Breadcrumb as AntdBreadcrumb } from 'antd';
|
import { Breadcrumb as AntdBreadcrumb } from 'antd';
|
||||||
import { useTheme } from 'antd-style';
|
import { useTheme } from 'antd-style';
|
||||||
import Link from 'next/link';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Flexbox } from 'react-layout-kit';
|
import { Flexbox } from 'react-layout-kit';
|
||||||
import urlJoin from 'url-join';
|
import urlJoin from 'url-join';
|
||||||
|
|
||||||
|
import Link from '@/app/[variants]/(main)/components/Link';
|
||||||
import { DiscoverTab } from '@/types/discover';
|
import { DiscoverTab } from '@/types/discover';
|
||||||
|
|
||||||
const Breadcrumb = memo<{ identifier: string; tab: DiscoverTab }>(({ tab, identifier }) => {
|
const Breadcrumb = memo<{ identifier: string; tab: DiscoverTab }>(({ tab, identifier }) => {
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { notFound } from 'next/navigation';
|
|
||||||
import { memo } from 'react';
|
|
||||||
import { Flexbox } from 'react-layout-kit';
|
|
||||||
|
|
||||||
import { withSuspense } from '@/components/withSuspense';
|
|
||||||
import { DetailProvider } from '@/features/MCPPluginDetail/DetailProvider';
|
|
||||||
import Header from '@/features/MCPPluginDetail/Header';
|
|
||||||
import { useFetchInstalledPlugins } from '@/hooks/useFetchInstalledPlugins';
|
|
||||||
import { useQuery } from '@/hooks/useQuery';
|
|
||||||
import { useDiscoverStore } from '@/store/discover';
|
|
||||||
import { DiscoverTab } from '@/types/discover';
|
|
||||||
|
|
||||||
import Breadcrumb from '../../features/Breadcrumb';
|
|
||||||
import { TocProvider } from '../../features/Toc/useToc';
|
|
||||||
import Details from './features/Details';
|
|
||||||
import Loading from './loading';
|
|
||||||
|
|
||||||
const Client = memo<{ identifier: string; mobile?: boolean }>(({ identifier, mobile }) => {
|
|
||||||
const { version } = useQuery() as { version?: string };
|
|
||||||
const useMcpDetail = useDiscoverStore((s) => s.useFetchMcpDetail);
|
|
||||||
const { data, isLoading } = useMcpDetail({ identifier, version });
|
|
||||||
|
|
||||||
useFetchInstalledPlugins();
|
|
||||||
|
|
||||||
if (isLoading) return <Loading />;
|
|
||||||
if (!data) return notFound();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TocProvider>
|
|
||||||
<DetailProvider config={data}>
|
|
||||||
{!mobile && <Breadcrumb identifier={identifier} tab={DiscoverTab.Mcp} />}
|
|
||||||
<Flexbox gap={16}>
|
|
||||||
<Header mobile={mobile} />
|
|
||||||
<Details mobile={mobile} />
|
|
||||||
</Flexbox>
|
|
||||||
</DetailProvider>
|
|
||||||
</TocProvider>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default withSuspense(Client);
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { DetailsLoading as default } from '../../../components/ListLoading';
|
|
||||||
+2
-2
@@ -5,8 +5,8 @@ import { Flexbox } from 'react-layout-kit';
|
|||||||
|
|
||||||
import { useDetailContext } from '@/features/MCPPluginDetail/DetailProvider';
|
import { useDetailContext } from '@/features/MCPPluginDetail/DetailProvider';
|
||||||
|
|
||||||
import McpList from '../../../../../../(list)/mcp/features/List';
|
import McpList from '../../../../../(list)/mcp/features/List';
|
||||||
import Title from '../../../../../../features/Title';
|
import Title from '../../../../../features/Title';
|
||||||
|
|
||||||
const Related = memo(() => {
|
const Related = memo(() => {
|
||||||
const { t } = useTranslation('discover');
|
const { t } = useTranslation('discover');
|
||||||
+5
-5
@@ -1,18 +1,18 @@
|
|||||||
import { Block, Icon, Tag } from '@lobehub/ui';
|
import { Block, Icon, Tag } from '@lobehub/ui';
|
||||||
import { useTheme } from 'antd-style';
|
import { useTheme } from 'antd-style';
|
||||||
import { CheckIcon, MinusIcon } from 'lucide-react';
|
import { CheckIcon, MinusIcon } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import qs from 'query-string';
|
import qs from 'query-string';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Flexbox } from 'react-layout-kit';
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import Link from '@/app/[variants]/(main)/components/Link';
|
||||||
|
import { usePathname } from '@/app/[variants]/(main)/hooks/usePathname';
|
||||||
import InlineTable from '@/components/InlineTable';
|
import InlineTable from '@/components/InlineTable';
|
||||||
|
|
||||||
import PublishedTime from '../../../../../../../../../../components/PublishedTime';
|
import PublishedTime from '../../../../../../../../../components/PublishedTime';
|
||||||
import { useDetailContext } from '../../../../../../../../../../features/MCPPluginDetail/DetailProvider';
|
import { useDetailContext } from '../../../../../../../../../features/MCPPluginDetail/DetailProvider';
|
||||||
import Title from '../../../../../../features/Title';
|
import Title from '../../../../../features/Title';
|
||||||
|
|
||||||
const Versions = memo(() => {
|
const Versions = memo(() => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
+1
-1
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useResponsive } from 'antd-style';
|
import { useResponsive } from 'antd-style';
|
||||||
import { useQueryState } from 'nuqs';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { Flexbox } from 'react-layout-kit';
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
@@ -10,6 +9,7 @@ import Nav from '@/features/MCPPluginDetail/Nav';
|
|||||||
import Overview from '@/features/MCPPluginDetail/Overview';
|
import Overview from '@/features/MCPPluginDetail/Overview';
|
||||||
import Schema from '@/features/MCPPluginDetail/Schema';
|
import Schema from '@/features/MCPPluginDetail/Schema';
|
||||||
import Score from '@/features/MCPPluginDetail/Score';
|
import Score from '@/features/MCPPluginDetail/Score';
|
||||||
|
import { useQueryState } from '@/hooks/useQueryParam';
|
||||||
import { McpNavKey } from '@/types/discover';
|
import { McpNavKey } from '@/types/discover';
|
||||||
|
|
||||||
import Sidebar from '../Sidebar';
|
import Sidebar from '../Sidebar';
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user