feat: Add Welcome page (#60)

*  feat: Implement chat interface, add session data selectors, and update pages

*  feat: 优化首页路由逻辑

* 💄 style: 优化 welcome 页面样式

* ♻️ refactor: 优化 chat 页面布局实现与样式

---------

Co-authored-by: arvinxx <arvinx@foxmail.com>
This commit is contained in:
CanisMinor
2023-08-11 23:49:07 +08:00
committed by GitHub
parent e8964184eb
commit 810ab0fe18
31 changed files with 550 additions and 73 deletions

View File

@@ -4,6 +4,7 @@
"401": "Sorry, the server has rejected your request. This may be due to insufficient permissions or invalid authentication.",
"403": "Sorry, the server has rejected your request. You do not have permission to access this content.",
"404": "Sorry, the server cannot find the page or resource you requested. Please make sure your URL is correct.",
"405": "Sorry, the server does not support the request method you are using. Please make sure your request method is correct.",
"429": "Sorry, your request is too frequent and the server is a bit tired. Please try again later.",
"500": "Sorry, the server seems to be experiencing some difficulties and is temporarily unable to complete your request. Please try again later.",
"502": "Sorry, the server seems to be lost and is temporarily unable to provide service. Please try again later.",

View File

@@ -1,4 +1,9 @@
{
"debug": {
"arguments": "Arguments",
"function_call": "Function Call",
"response": "Response"
},
"loading": {
"content": "Loading data...",
"plugin": "Plugin is running..."

View File

@@ -0,0 +1,14 @@
{
"button": {
"import": "Import Configuration",
"start": "Start Now"
},
"header": "Welcome",
"pickAgent": "Or choose from the following assistant templates",
"skip": "Skip Creation",
"slogan": {
"desc1": "Unlock the power of your brain and ignite your creativity. Your intelligent assistant is always here.",
"desc2": "Create your first assistant and let's get started~",
"title": "Give yourself a smarter brain"
}
}

View File

@@ -4,6 +4,7 @@
"401": "很抱歉,服务器拒绝了您的请求,可能是因为您的权限不足或未提供有效的身份验证",
"403": "很抱歉,服务器拒绝了您的请求,您没有访问此内容的权限 ",
"404": "很抱歉,服务器找不到您请求的页面或资源,请确认您的 URL 是否正确",
"405": "很抱歉,服务器不支持您使用的请求方法,请确认您的请求方法是否正确",
"429": "很抱歉,您的请求太多,服务器有点累了,请稍后再试",
"500": "很抱歉,服务器似乎遇到了一些困难,暂时无法完成您的请求,请稍后再试",
"502": "很抱歉,服务器似乎迷失了方向,暂时无法提供服务,请稍后再试",

View File

@@ -1,4 +1,9 @@
{
"debug": {
"arguments": "调用参数",
"function_call": "函数调用",
"response": "返回结果"
},
"loading": {
"content": "数据获取中...",
"plugin": "插件运行中..."

View File

@@ -0,0 +1,14 @@
{
"button": {
"import": "导入配置",
"start": "立即开始"
},
"header": "欢迎使用",
"pickAgent": "或从下列助手模板选择",
"skip": "跳过创建",
"slogan": {
"desc1": "开启大脑集群,激发思维火花。你的智能助理,一直都在。",
"desc2": "创建你的第一个助手,让我们开始吧~",
"title": "给自己一个更聪明的大脑"
}
}

View File

@@ -0,0 +1,15 @@
import { type LucideIcon, createLucideIcon } from 'lucide-react';
const Discord: LucideIcon = createLucideIcon('Discord', [
[
'path',
{
d: 'M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z',
key: '18tl5t',
},
],
]);
const DiscordIcon = (props: LucideIcon) => <Discord style={{ overflow: 'visible' }} {...props} />;
export default DiscordIcon as LucideIcon;

7
src/const/url.ts Normal file
View File

@@ -0,0 +1,7 @@
import pkg from '../../package.json';
export const GITHUB = pkg.homepage;
export const CHANGELOG = `${pkg.homepage}/blob/master/CHANGELOG.md`;
export const ABOUT = pkg.homepage;
export const FEEDBACK = pkg.bugs.url;
export const DISCORD = 'https://discord.gg/AYFPHvv2jT';

View File

@@ -14,12 +14,12 @@ import Router from 'next/router';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import DiscordIcon from '@/components/DiscordIcon';
import { ABOUT, CHANGELOG, DISCORD, FEEDBACK, GITHUB } from '@/const/url';
import { useExportConfig } from '@/hooks/useExportConfig';
import { useImportConfig } from '@/hooks/useImportConfig';
import { SettingsStore } from '@/store/settings';
import pkg from '../../../package.json';
export interface BottomActionProps {
setTab: SettingsStore['switchSideBar'];
tab: SettingsStore['sidebarKey'];
@@ -79,19 +79,19 @@ const BottomActions = memo<BottomActionProps>(({ tab, setTab }) => {
icon: <Icon icon={Feather} />,
key: 'feedback',
label: t('feedback'),
onClick: () => window.open(pkg.bugs.url, '__blank'),
onClick: () => window.open(FEEDBACK, '__blank'),
},
{
icon: <Icon icon={FileClock} />,
key: 'changelog',
label: t('changelog'),
onClick: () => window.open(`${pkg.homepage}/blob/master/CHANGELOG.md`, '__blank'),
onClick: () => window.open(CHANGELOG, '__blank'),
},
{
icon: <Icon icon={Heart} />,
key: 'about',
label: t('about'),
onClick: () => window.open(pkg.homepage, '__blank'),
onClick: () => window.open(ABOUT, '__blank'),
},
{
type: 'divider',
@@ -111,7 +111,8 @@ const BottomActions = memo<BottomActionProps>(({ tab, setTab }) => {
return (
<>
<ActionIcon icon={Github} onClick={() => window.open(pkg.homepage, '__blank')} />
<ActionIcon icon={DiscordIcon} onClick={() => window.open(DISCORD, '__blank')} />
<ActionIcon icon={Github} onClick={() => window.open(GITHUB, '__blank')} />
<Dropdown arrow={false} menu={{ items }} trigger={['click']}>
<ActionIcon active={tab === 'settings'} icon={Settings2} />
</Dropdown>

View File

@@ -23,7 +23,7 @@ const TopActions = memo<TopActionProps>(({ tab, setTab }) => {
// 如果已经在 chat 路径下了,那么就不用再跳转了
if (Router.asPath.startsWith('/chat')) return;
Router.push('/');
Router.push('/chat');
}}
size="large"
title={t('tab.chat')}

15
src/layout/AppLayout.tsx Normal file
View File

@@ -0,0 +1,15 @@
import { PropsWithChildren, memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import SideBar from '@/features/SideBar';
const AppLayout = memo<PropsWithChildren>(({ children }) => {
return (
<Flexbox horizontal width={'100%'}>
<SideBar />
{children}
</Flexbox>
);
});
export default AppLayout;

View File

@@ -4,6 +4,7 @@ export default {
agentMaxToken: '会话最大长度',
agentModel: '模型',
agentProfile: '助手信息',
appInitializing: '应用加载中...',
archive: '归档',
autoGenerate: '自动补全',
autoGenerateTooltip: '基于提示词自动补全助手描述',

View File

@@ -0,0 +1,14 @@
export default {
button: {
import: '导入配置',
start: '立即开始',
},
header: '欢迎使用',
pickAgent: '或从下列助手模板选择',
skip: '跳过创建',
slogan: {
desc1: '开启大脑集群,激发思维火花。你的智能助理,一直都在。',
desc2: '创建你的第一个助手,让我们开始吧~',
title: '给自己一个更聪明的大脑',
},
};

View File

@@ -3,6 +3,7 @@ import empty from '../default/empty';
import error from '../default/error';
import plugin from '../default/plugin';
import setting from '../default/setting';
import welcome from '../default/welcome';
const resources = {
common,
@@ -10,6 +11,7 @@ const resources = {
error,
plugin,
setting,
welcome,
} as const;
export default resources;

View File

@@ -8,7 +8,7 @@ import SessionItem from './SessionItem';
import SkeletonItem from './SkeletonItem';
const SessionList = memo(() => {
const list = useSessionStore((s) => sessionSelectors.sessionList(s), isEqual);
const list = useSessionStore(sessionSelectors.sessionList, isEqual);
const [activeId, loading] = useSessionStore((s) => [s.activeId, s.autocompleteLoading.title]);
const isInit = useSessionHydrated();

View File

@@ -28,13 +28,28 @@ const useStyles = createStyles(({ css, token }) => ({
opacity: 1;
}
`,
promptBox: css`
position: relative;
overflow: hidden;
border-bottom: 1px solid ${token.colorBorder};
`,
promptMask: css`
pointer-events: none;
position: absolute;
z-index: 10;
bottom: 0;
left: 0;
width: 100%;
height: 32px;
background: linear-gradient(to bottom, transparent, ${token.colorBgLayout});
`,
title: css`
font-size: ${token.fontSizeHeading4}px;
font-weight: bold;
`,
topic: css`
border-top: 1px solid ${token.colorBorder};
`,
}));
const Inner = memo(() => {
@@ -60,33 +75,36 @@ const Inner = memo(() => {
}
title={t('settingAgent.prompt.title', { ns: 'setting' })}
/>
<Flexbox height={200} padding={'0 16px 16px'}>
<Flexbox className={styles.promptBox} height={200} padding={'0 16px 16px'}>
{hydrated ? (
<EditableMessage
classNames={{ markdown: styles.prompt }}
onChange={(e) => {
updateAgentConfig({ systemRole: e });
}}
onOpenChange={setOpenModal}
openModal={openModal}
placeholder={`${t('settingAgent.prompt.placeholder', { ns: 'setting' })}...`}
styles={{ markdown: systemRole ? {} : { opacity: 0.5 } }}
text={{
cancel: t('cancel'),
confirm: t('ok'),
edit: t('edit'),
title: t('settingAgent.prompt.title', { ns: 'setting' }),
}}
value={systemRole}
/>
<>
<EditableMessage
classNames={{ markdown: styles.prompt }}
onChange={(e) => {
updateAgentConfig({ systemRole: e });
}}
onOpenChange={setOpenModal}
openModal={openModal}
placeholder={`${t('settingAgent.prompt.placeholder', { ns: 'setting' })}...`}
styles={{ markdown: systemRole ? {} : { opacity: 0.5 } }}
text={{
cancel: t('cancel'),
confirm: t('ok'),
edit: t('edit'),
title: t('settingAgent.prompt.title', { ns: 'setting' }),
}}
value={systemRole}
/>
<div className={styles.promptMask} />
</>
) : (
<Skeleton active avatar={false} style={{ marginTop: 12 }} title={false} />
)}
</Flexbox>
<Flexbox className={styles.topic} gap={16} padding={16}>
<Flexbox gap={16} padding={16}>
<SearchBar placeholder={t('topic.searchPlaceholder')} spotlight type={'ghost'} />
{!hydrated ? (
<Flexbox gap={8} style={{ marginTop: 12 }}>
<Flexbox gap={8} style={{ marginTop: 8 }}>
{Array.from({ length: 8 }).map((_, i) => (
<Skeleton
active

View File

@@ -5,10 +5,10 @@ import { Flexbox } from 'react-layout-kit';
import { agentSelectors, useSessionStore } from '@/store/session';
import { genSiteHeadTitle } from '@/utils/genSiteHeadTitle';
import Layout from '../layout';
import Conversation from './Conversation';
import Header from './Header';
import Config from './Sidebar';
import Layout from './layout';
const Chat = memo(() => {
const [avatar, title] = useSessionStore((s) => [

View File

@@ -1,24 +1,18 @@
import { useRouter } from 'next/router';
import { PropsWithChildren, memo, useEffect } from 'react';
import { Flexbox } from 'react-layout-kit';
import { shallow } from 'zustand/shallow';
import SideBar from '@/features/SideBar';
import { createI18nNext } from '@/locales/create';
import AppLayout from '@/layout/AppLayout';
import { useSessionStore } from '@/store/session';
import { useSettings } from '@/store/settings';
import { Sessions } from './SessionList';
const initI18n = createI18nNext();
import { Sessions } from '../SessionList';
const ChatLayout = memo<PropsWithChildren>(({ children }) => {
useEffect(() => {
initI18n.finally();
}, []);
const [activeSession, toggleTopic] = useSessionStore((s) => {
return [s.activeSession, s.toggleTopic];
});
}, shallow);
const router = useRouter();
const { id } = router.query;
@@ -45,13 +39,12 @@ const ChatLayout = memo<PropsWithChildren>(({ children }) => {
}, []);
return (
<Flexbox horizontal width={'100%'}>
<SideBar />
<AppLayout>
<Sessions />
<Flexbox flex={1} height={'100vh'} style={{ position: 'relative' }}>
{children}
</Flexbox>
</Flexbox>
</AppLayout>
);
});

View File

@@ -7,10 +7,10 @@ import { Flexbox } from 'react-layout-kit';
import HeaderSpacing from '@/components/HeaderSpacing';
import { HEADER_HEIGHT } from '@/const/layoutTokens';
import { AgentConfig, AgentMeta, AgentPlugin, AgentPrompt } from '@/features/AgentSetting';
import AppLayout from '@/layout/AppLayout';
import { agentSelectors, useSessionStore } from '@/store/session';
import { genSiteHeadTitle } from '@/utils/genSiteHeadTitle';
import ChatLayout from '../../layout';
import Header from './Header';
const EditPage = memo(() => {
@@ -32,7 +32,7 @@ const EditPage = memo(() => {
<Head>
<title>{pageTitle}</title>
</Head>
<ChatLayout>
<AppLayout>
<Header />
<Flexbox align={'center'} flex={1} gap={16} padding={24} style={{ overflow: 'auto' }}>
<HeaderSpacing height={HEADER_HEIGHT - 16} />
@@ -46,7 +46,7 @@ const EditPage = memo(() => {
<AgentConfig config={config} updateConfig={updateAgentConfig} />
<AgentPlugin config={config} updateConfig={toggleAgentPlugin} />
</Flexbox>
</ChatLayout>
</AppLayout>
</>
);
});

View File

@@ -1,24 +1 @@
import Router from 'next/router';
import { useEffect } from 'react';
import { sessionSelectors, useSessionStore } from '@/store/session';
import Chat from './[id]/index.page';
export default () => {
// 支持用户在进到首页时,自动激活列表中的第一个角色
useEffect(() => {
const hasRehydrated = useSessionStore.persist.hasHydrated();
// 只有当水合完毕后,才往下走
if (!hasRehydrated) return;
// 如果当前有会话,那么就激活第一个会话
const list = sessionSelectors.sessionList(useSessionStore.getState());
if (list.length > 0) {
const sessionId = list[0].id;
Router.push(`/chat/${sessionId}`);
}
}, []);
return <Chat />;
};
export { default } from './[id]/index.page';

View File

@@ -1 +1,33 @@
export { default } from './chat/index.page';
import { Icon } from '@lobehub/ui';
import { useWhyDidYouUpdate } from 'ahooks';
import { Loader2 } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Center } from 'react-layout-kit';
import { sessionSelectors, useSessionHydrated, useSessionStore } from '@/store/session';
import Chat from './chat/index.page';
import Welcome from './welcome/index.page';
const Loading = memo(() => {
const { t } = useTranslation('common');
return (
<Center gap={12} height={'100vh'} width={'100%'}>
<Icon icon={Loader2} size={{ fontSize: 64 }} spin />
{t('appInitializing')}
</Center>
);
});
export default memo(() => {
const hydrated = useSessionHydrated();
const hasSession = useSessionStore(sessionSelectors.hasSessionList);
useWhyDidYouUpdate('index.page', { hasSession, hydrated });
if (!hydrated) return <Loading />;
return !hasSession ? <Welcome /> : <Chat />;
});

View File

@@ -0,0 +1,71 @@
import { Avatar, Spotlight } from '@lobehub/ui';
import { Typography } from 'antd';
import { createStyles } from 'antd-style';
import { rgba } from 'polished';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import { LobeAgentSession } from '@/types/session';
const { Paragraph, Title } = Typography;
const useStyles = createStyles(({ css, token, cx, stylish }) => ({
container: cx(
stylish.blur,
css`
cursor: pointer;
position: relative;
overflow: hidden;
flex: 1;
padding: 16px;
background-color: ${rgba(token.colorBgContainer, 0.5)};
border: 1px solid ${rgba(token.colorText, 0.2)};
border-radius: ${token.borderRadiusLG}px;
transition: all 400ms ${token.motionEaseOut};
&:hover {
background-color: ${rgba(token.colorBgElevated, 0.2)};
border-color: ${token.colorText};
box-shadow: 0 0 0 1px ${token.colorText};
}
,
&:active {
scale: 0.98;
}
`,
),
desc: css`
margin: 0 !important;
`,
title: css`
margin: 0 !important;
`,
}));
export interface AgentCardProps {
meta: LobeAgentSession['meta'];
}
const AgentCard = memo<AgentCardProps>(({ meta }) => {
const { styles } = useStyles();
return (
<Flexbox className={styles.container} gap={8}>
<Spotlight size={200} />
<Avatar avatar={meta.avatar} background={meta.backgroundColor} />
<Title className={styles.title} ellipsis level={5}>
{meta.title}
</Title>
<Paragraph className={styles.desc} ellipsis={{ rows: 2 }} type="secondary">
{meta.description}
</Paragraph>
</Flexbox>
);
});
export default AgentCard;

View File

@@ -0,0 +1,43 @@
import { useResponsive } from 'antd-style';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import { useStyles } from '../style';
import AgentCard, { type AgentCardProps } from './AgentCard';
const items: AgentCardProps['meta'][] = [
{
avatar: '😀',
description: 'dddddd',
title: 'Title',
},
{
avatar: '😀',
description: 'dddddd',
title: 'Title',
},
{
avatar: '😀',
description: 'dddddd',
title: 'Title',
},
];
const AgentTemplate = memo<{ width: number }>(({ width }) => {
const { styles } = useStyles();
const { mobile } = useResponsive();
return (
<Flexbox
className={styles.templateContainer}
gap={16}
horizontal={!mobile}
style={{ marginTop: -width / 20 }}
>
{items.map((meta, index) => (
<AgentCard key={index} meta={meta} />
))}
</Flexbox>
);
});
export default AgentTemplate;

View File

@@ -0,0 +1,39 @@
import { LogoThree } from '@lobehub/ui';
import { useResponsive } from 'antd-style';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { genSize, useStyles } from '../style';
const Hero = memo<{ width: number }>(({ width }) => {
const size = useMemo(
() => ({
base: genSize(width / 3.5, 240),
desc: genSize(width / 50, 14),
logo: genSize(width / 3.8, 180),
title: genSize(width / 20, 32),
}),
[width],
);
const { styles } = useStyles(size.base);
const { mobile } = useResponsive();
const { t } = useTranslation('welcome');
return (
<>
<LogoThree
size={size.logo}
style={{ marginBottom: -size.logo / 40, marginTop: -size.logo / 5 }}
/>
<div className={styles.title} style={{ fontSize: size.title }}>
LobeChat{mobile ? <br /> : ' '}
{t('slogan.title')}
</div>
<div className={styles.desc} style={{ fontSize: size.desc }}>
{t('slogan.desc1')}
</div>
</>
);
});
export default Hero;

View File

@@ -0,0 +1,56 @@
import { GridShowcase, Icon } from '@lobehub/ui';
import { useSize } from 'ahooks';
import { Button, Upload } from 'antd';
import { SendHorizonal } from 'lucide-react';
import Link from 'next/link';
import Router from 'next/router';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import { useImportConfig } from '@/hooks/useImportConfig';
import { useStyles } from '../style';
import AgentTemplate from './AgentTemplate';
import Hero from './Hero';
const Banner = memo(() => {
const { importConfig } = useImportConfig();
const ref = useRef(null);
const domSize = useSize(ref);
const width = domSize?.width || 1024;
const { t } = useTranslation('welcome');
const { styles } = useStyles();
const handleImport = useCallback((e: any) => {
importConfig(e);
Router.push('/chat');
}, []);
return (
<>
<GridShowcase>
<div className={styles.container} ref={ref}>
<Hero width={width} />
</div>
<Flexbox gap={16} horizontal style={{ marginTop: 16 }}>
<Link href={'/chat'}>
<Button size={'large'} type={'primary'}>
<Flexbox align={'center'} gap={4} horizontal>
{t('button.start')}
<Icon icon={SendHorizonal} />
</Flexbox>
</Button>
</Link>
<Upload maxCount={1} onChange={handleImport} showUploadList={false}>
<Button size={'large'}>{t('button.import')}</Button>
</Upload>
</Flexbox>
</GridShowcase>
<AgentTemplate width={width} />
</>
);
});
export default Banner;

View File

@@ -0,0 +1,24 @@
import { ActionIcon } from '@lobehub/ui';
import { useTheme } from 'antd-style';
import { Book, Github } from 'lucide-react';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import Discord from '@/components/DiscordIcon';
import { CHANGELOG, DISCORD, GITHUB } from '@/const/url';
const Footer = memo(() => {
const theme = useTheme();
return (
<Flexbox align={'center'} horizontal justify={'space-between'} style={{ padding: 16 }}>
<span style={{ color: theme.colorTextDescription }}>©{new Date().getFullYear()} LobeHub</span>
<Flexbox horizontal>
<ActionIcon icon={Discord} onClick={() => window.open(DISCORD, '__blank')} size={'site'} />
<ActionIcon icon={Book} onClick={() => window.open(CHANGELOG, '__blank')} size={'site'} />
<ActionIcon icon={Github} onClick={() => window.open(GITHUB, '__blank')} size={'site'} />
</Flexbox>
</Flexbox>
);
});
export default Footer;

View File

@@ -0,0 +1,28 @@
import Head from 'next/head';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { genSiteHeadTitle } from '@/utils/genSiteHeadTitle';
import Banner from './Banner';
import Footer from './Footer';
import Layout from './layout';
const Welcome = memo(() => {
const { t } = useTranslation('welcome');
const pageTitle = genSiteHeadTitle(t('header'));
return (
<>
<Head>
<title>{pageTitle}</title>
</Head>
<Layout>
<Banner />
<Footer />
</Layout>
</>
);
});
export default Welcome;

View File

@@ -0,0 +1,31 @@
import { Logo } from '@lobehub/ui';
import Link from 'next/link';
import { PropsWithChildren, memo } from 'react';
import { Center, Flexbox } from 'react-layout-kit';
import AppLayout from '../../layout/AppLayout';
import { useStyles } from './style';
const WelcomeLayout = memo<PropsWithChildren>(({ children }) => {
const { styles } = useStyles();
return (
<AppLayout>
<Center
className={styles.layout}
flex={1}
height={'100vh'}
horizontal
style={{ position: 'relative' }}
>
<Link href={'/'}>
<Logo className={styles.logo} size={36} type={'text'} />
</Link>
<Flexbox className={styles.view} flex={1} style={{ maxWidth: 1024 }}>
{children}
</Flexbox>
</Center>
</AppLayout>
);
});
export default WelcomeLayout;

View File

@@ -0,0 +1,63 @@
import { createStyles } from 'antd-style';
import { rgba } from 'polished';
export const useStyles = createStyles(({ css, token, stylish, cx }) => {
return {
container: css`
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-bottom: 24px;
`,
desc: css`
font-weight: 400;
color: ${rgba(token.colorText, 0.8)};
text-align: center;
`,
layout: css`
background: ${token.colorBgContainer};
`,
logo: css`
position: absolute;
top: 16px;
left: 16px;
fill: ${token.colorText};
`,
note: css`
z-index: 10;
margin-top: 16px;
color: ${token.colorTextDescription};
`,
skip: css`
color: ${token.colorTextDescription};
`,
templateContainer: css`
flex-wrap: wrap;
width: 100%;
padding: 16px;
`,
title: css`
margin-bottom: 0.2em;
font-weight: 800;
line-height: 1.2;
text-align: center;
`,
view: cx(
stylish.noScrollbar,
css`
overflow-x: hidden;
overflow-y: auto;
height: 100vh;
padding: 32px 16px;
`,
),
};
});
export const genSize = (size: number, minSize: number) => {
return size < minSize ? minSize : size;
};

View File

@@ -4,6 +4,7 @@ import {
currentSessionSafe,
getSessionById,
getSessionMetaById,
hasSessionList,
sessionList,
} from './list';
@@ -15,5 +16,6 @@ export const sessionSelectors = {
getExportAgent,
getSessionById,
getSessionMetaById,
hasSessionList,
sessionList,
};

View File

@@ -36,6 +36,11 @@ export const sessionList = (s: SessionStore) => {
});
};
export const hasSessionList = (s: SessionStore) => {
const list = sessionList(s);
return list?.length > 0;
};
export const getSessionById =
(id: string) =>
(s: SessionStore): LobeAgentSession => {