mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
✨ 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:
@@ -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.",
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"debug": {
|
||||
"arguments": "Arguments",
|
||||
"function_call": "Function Call",
|
||||
"response": "Response"
|
||||
},
|
||||
"loading": {
|
||||
"content": "Loading data...",
|
||||
"plugin": "Plugin is running..."
|
||||
|
||||
14
locales/en_US/welcome.json
Normal file
14
locales/en_US/welcome.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
"401": "很抱歉,服务器拒绝了您的请求,可能是因为您的权限不足或未提供有效的身份验证",
|
||||
"403": "很抱歉,服务器拒绝了您的请求,您没有访问此内容的权限 ",
|
||||
"404": "很抱歉,服务器找不到您请求的页面或资源,请确认您的 URL 是否正确",
|
||||
"405": "很抱歉,服务器不支持您使用的请求方法,请确认您的请求方法是否正确",
|
||||
"429": "很抱歉,您的请求太多,服务器有点累了,请稍后再试",
|
||||
"500": "很抱歉,服务器似乎遇到了一些困难,暂时无法完成您的请求,请稍后再试",
|
||||
"502": "很抱歉,服务器似乎迷失了方向,暂时无法提供服务,请稍后再试",
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"debug": {
|
||||
"arguments": "调用参数",
|
||||
"function_call": "函数调用",
|
||||
"response": "返回结果"
|
||||
},
|
||||
"loading": {
|
||||
"content": "数据获取中...",
|
||||
"plugin": "插件运行中..."
|
||||
|
||||
14
locales/zh_CN/welcome.json
Normal file
14
locales/zh_CN/welcome.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"button": {
|
||||
"import": "导入配置",
|
||||
"start": "立即开始"
|
||||
},
|
||||
"header": "欢迎使用",
|
||||
"pickAgent": "或从下列助手模板选择",
|
||||
"skip": "跳过创建",
|
||||
"slogan": {
|
||||
"desc1": "开启大脑集群,激发思维火花。你的智能助理,一直都在。",
|
||||
"desc2": "创建你的第一个助手,让我们开始吧~",
|
||||
"title": "给自己一个更聪明的大脑"
|
||||
}
|
||||
}
|
||||
15
src/components/DiscordIcon/index.tsx
Normal file
15
src/components/DiscordIcon/index.tsx
Normal 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
7
src/const/url.ts
Normal 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';
|
||||
@@ -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>
|
||||
|
||||
@@ -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
15
src/layout/AppLayout.tsx
Normal 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;
|
||||
@@ -4,6 +4,7 @@ export default {
|
||||
agentMaxToken: '会话最大长度',
|
||||
agentModel: '模型',
|
||||
agentProfile: '助手信息',
|
||||
appInitializing: '应用加载中...',
|
||||
archive: '归档',
|
||||
autoGenerate: '自动补全',
|
||||
autoGenerateTooltip: '基于提示词自动补全助手描述',
|
||||
|
||||
14
src/locales/default/welcome.ts
Normal file
14
src/locales/default/welcome.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export default {
|
||||
button: {
|
||||
import: '导入配置',
|
||||
start: '立即开始',
|
||||
},
|
||||
header: '欢迎使用',
|
||||
pickAgent: '或从下列助手模板选择',
|
||||
skip: '跳过创建',
|
||||
slogan: {
|
||||
desc1: '开启大脑集群,激发思维火花。你的智能助理,一直都在。',
|
||||
desc2: '创建你的第一个助手,让我们开始吧~',
|
||||
title: '给自己一个更聪明的大脑',
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) => [
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 />;
|
||||
});
|
||||
|
||||
71
src/pages/welcome/Banner/AgentCard.tsx
Normal file
71
src/pages/welcome/Banner/AgentCard.tsx
Normal 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;
|
||||
43
src/pages/welcome/Banner/AgentTemplate.tsx
Normal file
43
src/pages/welcome/Banner/AgentTemplate.tsx
Normal 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;
|
||||
39
src/pages/welcome/Banner/Hero.tsx
Normal file
39
src/pages/welcome/Banner/Hero.tsx
Normal 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;
|
||||
56
src/pages/welcome/Banner/index.tsx
Normal file
56
src/pages/welcome/Banner/index.tsx
Normal 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;
|
||||
24
src/pages/welcome/Footer/index.tsx
Normal file
24
src/pages/welcome/Footer/index.tsx
Normal 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;
|
||||
28
src/pages/welcome/index.page.tsx
Normal file
28
src/pages/welcome/index.page.tsx
Normal 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;
|
||||
31
src/pages/welcome/layout.tsx
Normal file
31
src/pages/welcome/layout.tsx
Normal 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;
|
||||
63
src/pages/welcome/style.ts
Normal file
63
src/pages/welcome/style.ts
Normal 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;
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
currentSessionSafe,
|
||||
getSessionById,
|
||||
getSessionMetaById,
|
||||
hasSessionList,
|
||||
sessionList,
|
||||
} from './list';
|
||||
|
||||
@@ -15,5 +16,6 @@ export const sessionSelectors = {
|
||||
getExportAgent,
|
||||
getSessionById,
|
||||
getSessionMetaById,
|
||||
hasSessionList,
|
||||
sessionList,
|
||||
};
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user