feat: Introduce new features and styles for chat application

The changes include importing new components, modifying existing styles, and introducing new components. The code snippets provided show changes made to various files in a chat application, including changes to ActionIcon components, header components, session item components, and the structure and styling of the chat session list component. The code changes also involve the addition of new files for CSS overrides and global styles, as well as modifications to index pages and chat session selectors.

The purpose of these changes is to enhance the functionality and user experience of the chat application by introducing new features and improving the overall styling.
This commit is contained in:
canisminor1990
2023-07-15 21:36:50 +08:00
parent 85e91ad977
commit cef01c0263
23 changed files with 339 additions and 229 deletions

View File

@@ -3,11 +3,15 @@ const i18n = require('./.i18nrc');
/** @type {import('next-i18next').UserConfig} */
module.exports = {
debug: process.env.NODE_ENV === 'development',
fallbackLng: {
default: ['en'],
zh_TW: ['zh_CN'],
},
i18n: {
defaultLocale: i18n.entryLocale,
locales: [i18n.entryLocale, ...i18n.outputLocales],
},
localePath:
typeof window === 'undefined' ? require('node:path').resolve('./', i18n.output) : '/locales',
typeof window === 'undefined' ? require('node:path').resolve('./public/locales') : '/locales',
reloadOnPrerender: process.env.NODE_ENV === 'development',
};

View File

@@ -1,4 +1,16 @@
{
"agentProfile": "Agent Profile",
"archive": "Archive",
"cancel": "Cancel",
"close": "Close",
"confirmRemoveSessionItemAlert": "You are about to remove this agent. Once removed, it cannot be recovered. Please confirm your action.",
"defaultAgent": "Default Agent",
"edit": "Edit",
"newAgent": "New Agent",
"noDescription": "No description",
"ok": "OK",
"searchAgentPlaceholder": "Search agents and conversations...",
"sessionSetting": "Session Setting",
"setting": "Setting",
"share": "Share"
}

View File

@@ -1,4 +1,16 @@
{
"agentProfile": "エージェントプロファイル",
"archive": "アーカイブ",
"cancel": "キャンセル",
"close": "閉じる",
"confirmRemoveSessionItemAlert": "このエージェントを削除します。削除後は元に戻すことはできません。操作を確認してください",
"defaultAgent": "デフォルトエージェント",
"edit": "編集",
"newAgent": "新しいエージェント",
"noDescription": "説明はありません",
"ok": "OK",
"searchAgentPlaceholder": "エージェントと会話を検索...",
"sessionSetting": "セッション設定",
"setting": "設定",
"share": "共有する"
}

View File

@@ -1,4 +1,16 @@
{
"agentProfile": "도우미 프로필",
"archive": "보관",
"cancel": "취소",
"close": "닫기",
"confirmRemoveSessionItemAlert": "이 도우미를 삭제하려고 합니다. 삭제 후에는 복구할 수 없으므로 작업을 확인하십시오.",
"defaultAgent": "기본 도우미",
"edit": "편집",
"newAgent": "새 도우미",
"noDescription": "설명 없음",
"ok": "확인",
"searchAgentPlaceholder": "도우미 및 대화 검색...",
"sessionSetting": "세션 설정",
"setting": "설정",
"share": "공유"
}

View File

@@ -1,4 +1,16 @@
{
"agentProfile": "助手信息",
"archive": "归档",
"cancel": "取消",
"close": "关闭",
"confirmRemoveSessionItemAlert": "即将删除该助手,删除后该将无法找回,请确认你的操作",
"defaultAgent": "默认助手",
"edit": "编辑",
"newAgent": "新建助手",
"noDescription": "暂无描述",
"ok": "确定",
"searchAgentPlaceholder": "搜索助手和对话...",
"sessionSetting": "会话设置",
"setting": "设置",
"share": "分享"
}

View File

@@ -1,4 +1,16 @@
{
"agentProfile": "助手資訊",
"archive": "歸檔",
"cancel": "取消",
"close": "關閉",
"confirmRemoveSessionItemAlert": "即將刪除該助手,刪除後將無法找回,請確認你的操作",
"defaultAgent": "默認助手",
"edit": "編輯",
"newAgent": "新建助手",
"noDescription": "暫無描述",
"ok": "確定",
"searchAgentPlaceholder": "搜索助手和對話...",
"sessionSetting": "會話設置",
"setting": "設置",
"share": "分享"
}

View File

@@ -10,6 +10,7 @@ export const useStyles = createStyles(({ css, token }) => ({
panel: css`
height: 100vh;
color: ${token.colorTextSecondary};
background: ${token.colorBgContainer};
`,
}));

View File

@@ -5,7 +5,9 @@ import Zh_CN from 'antd/locale/zh_CN';
import { PropsWithChildren, useEffect } from 'react';
import { useChatStore } from 'src/store/session';
import { GlobalStyle, useStyles } from './style';
import { GlobalStyle } from '@/styles';
import { useStyles } from './style';
const Layout = ({ children }: PropsWithChildren) => {
const { styles } = useStyles();

View File

@@ -1,7 +1,4 @@
import { createGlobalStyle, createStyles } from 'antd-style';
import { rgba } from 'polished';
export const NOTIFICATION_PRIMARY = 'notification-primary-info';
import { createStyles } from 'antd-style';
export const useStyles = createStyles(({ css, token }) => ({
bg: css`
@@ -13,82 +10,9 @@ export const useStyles = createStyles(({ css, token }) => ({
height: 100%;
background: ${token.colorBgLayout};
background-image: linear-gradient(
180deg,
${token.colorBgContainer} 0%,
rgba(255, 255, 255, 0%) 20%
);
:has(#ChatLayout, #FlowLayout) {
overflow: hidden;
}
`,
}));
export const GlobalStyle = createGlobalStyle`
.ant-btn {
box-shadow: none;
}
#__next {
height: 100%;
}
p {
margin-bottom: 0;
}
li {
display: block;
}
.ant-btn-default:not(:disabled):not(.ant-btn-dangerous) {
border-color: transparent;
&:hover {
color: ${(p) => p.theme.colorText};
background: ${({ theme }) => (theme.isDarkMode ? theme.colorFill : theme.colorFillTertiary)};
border-color: transparent;
}
}
.ant-popover {
z-index: 1100;
}
/* 定义滚动槽的样式 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
margin-right: 4px;
background-color: transparent; /* 定义滚动槽的背景色 */
&-thumb {
background-color: ${({ theme }) => theme.colorFill}; /* 定义滚动块的背景色 */
border-radius: 4px; /* 定义滚动块的圆角半径 */
}
&-corner {
display: none;
}
}
.ant-notification .ant-notification-notice.${NOTIFICATION_PRIMARY} {
background: ${(p) => p.theme.colorPrimary};
box-shadow: 0 6px 16px 0 ${({ theme }) => rgba(theme.colorPrimary, 0.1)},
0 3px 6px -4px ${({ theme }) => rgba(theme.colorPrimary, 0.2)},
0 9px 28px 8px ${({ theme }) => rgba(theme.colorPrimary, 0.1)};
.anticon {
color: ${(p) => p.theme.colorTextLightSolid}
}
.ant-notification-notice-message {
margin-bottom: 0;
padding-right: 0;
color: ${(p) => p.theme.colorTextLightSolid};
}
}
`;

View File

@@ -1,6 +1,7 @@
import { ActionIcon, DraggablePanel } from '@lobehub/ui';
import { ActionIcon, DraggablePanel, DraggablePanelContainer } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { LucideEdit, LucideX } from 'lucide-react';
import { useTranslation } from 'next-i18next';
import { Flexbox } from 'react-layout-kit';
import { shallow } from 'zustand/shallow';
@@ -8,9 +9,11 @@ import { useChatStore } from '@/store/session';
import ReadMode from './ReadMode';
const WIDTH = 280;
const useStyles = createStyles(({ css, token }) => ({
drawer: css`
background: ${token.colorBgContainer};
background: ${token.colorBgLayout};
`,
header: css`
border-bottom: 1px solid ${token.colorBorder};
@@ -18,6 +21,7 @@ const useStyles = createStyles(({ css, token }) => ({
}));
const Config = () => {
const { t } = useTranslation('common');
const { styles } = useStyles();
const [showAgentSettings, toggleConfig] = useChatStore(
(s) => [s.showAgentSettings, s.toggleConfig],
@@ -29,32 +33,41 @@ const Config = () => {
className={styles.drawer}
expand={showAgentSettings}
expandable={false}
minWidth={400}
maxWidth={WIDTH}
minWidth={WIDTH}
mode={'float'}
pin
placement={'right'}
resize={{ left: false }}
>
<Flexbox
align={'center'}
className={styles.header}
distribution={'space-between'}
horizontal
padding={16}
>
<Flexbox></Flexbox>
<Flexbox horizontal>
<ActionIcon icon={LucideEdit} title={'编辑'} />
<ActionIcon
icon={LucideX}
onClick={() => {
toggleConfig(false);
}}
title={'关闭'}
/>
<DraggablePanelContainer style={{ flex: 'none', minWidth: WIDTH }}>
<Flexbox
align={'center'}
className={styles.header}
distribution={'space-between'}
horizontal
padding={12}
paddingInline={16}
>
<Flexbox>{t('agentProfile')}</Flexbox>
<Flexbox gap={4} horizontal>
<ActionIcon
icon={LucideEdit}
size={{ blockSize: 32, fontSize: 20 }}
title={t('edit')}
/>
<ActionIcon
icon={LucideX}
onClick={() => {
toggleConfig(false);
}}
size={{ blockSize: 32, fontSize: 20 }}
title={t('close')}
/>
</Flexbox>
</Flexbox>
</Flexbox>
<ReadMode />
<ReadMode />
</DraggablePanelContainer>
</DraggablePanel>
);
};

View File

@@ -44,8 +44,7 @@ const Header = memo(() => {
align={'center'}
distribution={'space-between'}
horizontal
padding={8}
paddingInline={16}
padding="8px 8px 8px 16px"
style={{
borderBottom: `1px solid ${theme.colorSplit}`,
gridArea: 'header',
@@ -54,20 +53,26 @@ const Header = memo(() => {
<Flexbox align={'center'} gap={12} horizontal>
<Avatar avatar={meta && sessionSelectors.getAgentAvatar(meta)} size={40} title={'123'} />
<Flexbox>
<Flexbox className={styles.title}>{meta?.title}</Flexbox>
<Flexbox className={styles.desc}>{meta?.description || '暂无描述'}</Flexbox>
<Flexbox className={styles.title}>{meta?.title || t('defaultAgent')}</Flexbox>
<Flexbox className={styles.desc}>{meta?.description || t('noDescription')}</Flexbox>
</Flexbox>
</Flexbox>
<Flexbox gap={16} horizontal>
<Flexbox gap={8} horizontal>
<ActionIcon
icon={Share2Icon}
onClick={() => {
// genShareUrl();
}}
size={{ fontSize: 24 }}
title={t('share')}
/>
<ActionIcon icon={ArchiveIcon} title={'归档'} />
<ActionIcon icon={MoreVerticalIcon} onClick={toggleConfig} />
<ActionIcon icon={ArchiveIcon} size={{ fontSize: 24 }} title={t('archive')} />
<ActionIcon
icon={MoreVerticalIcon}
onClick={toggleConfig}
size={{ fontSize: 24 }}
title={t('sessionSetting')}
/>
</Flexbox>
</Flexbox>
);

View File

@@ -1,6 +1,7 @@
import { ActionIcon, Logo, SearchBar } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { Plus } from 'lucide-react';
import { MessageSquarePlus } from 'lucide-react';
import { useTranslation } from 'next-i18next';
import Link from 'next/link';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
@@ -20,25 +21,32 @@ export const useStyles = createStyles(({ css, token }) => ({
const Header = memo(() => {
const { styles } = useStyles();
const { t } = useTranslation('common');
const [keywords, createSession] = useChatStore(
(s) => [s.searchKeywords, s.createSession],
shallow,
);
return (
<Flexbox className={styles.top} gap={16} padding={'16px 12px 0 16px'}>
<Flexbox className={styles.top} gap={16} padding={16}>
<Flexbox distribution={'space-between'} horizontal>
<Link href={'/'}>
<Logo className={styles.logo} size={36} type={'text'} />
</Link>
<ActionIcon icon={Plus} onClick={createSession} style={{ minWidth: 32 }} title={'新对话'} />
<ActionIcon
icon={MessageSquarePlus}
onClick={createSession}
size={{ fontSize: 24 }}
style={{ flex: 'none' }}
title={t('newAgent')}
/>
</Flexbox>
<SearchBar
allowClear
onChange={(e) => useChatStore.setState({ searchKeywords: e.target.value })}
placeholder="Search..."
type={'block'}
placeholder={t('searchAgentPlaceholder')}
spotlight
type={'ghost'}
value={keywords}
/>
</Flexbox>

View File

@@ -1,66 +1,31 @@
import { CloseOutlined } from '@ant-design/icons';
import { Avatar, List } from '@lobehub/ui';
import { ActionIcon, Avatar, List } from '@lobehub/ui';
import { Popconfirm } from 'antd';
import { createStyles } from 'antd-style';
import { X } from 'lucide-react';
import { useTranslation } from 'next-i18next';
import { FC, memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import { shallow } from 'zustand/shallow';
import { sessionSelectors, useChatStore } from '@/store/session';
const useStyles = createStyles(({ css, cx }) => {
const closeCtn = css`
position: absolute;
top: 50%;
left: 2px;
transform: translateY(-50%);
width: 16px;
height: 16px;
font-size: 10px;
opacity: 0;
`;
return {
active: css`
opacity: 1;
`,
closeCtn,
container: css`
position: relative;
&:hover {
.${cx(closeCtn)} {
opacity: 1;
}
}
`,
time: css`
align-self: flex-start;
`,
};
});
import { useStyles } from './style';
interface SessionItemProps {
active: boolean;
id: string;
loading: boolean;
simple?: boolean;
}
const SessionItem: FC<SessionItemProps> = memo(({ id, active, simple = true, loading }) => {
const SessionItem: FC<SessionItemProps> = memo(({ id, active = true, loading }) => {
const { t } = useTranslation('common');
const { styles, theme, cx } = useStyles();
const [title, systemRole, avatar, avatarBackground, updateAt, switchAgent, removeSession] =
useChatStore((s) => {
const session = sessionSelectors.getSessionById(id)(s);
const meta = session.meta;
const systemRole = session.config.systemRole;
return [
meta.title || systemRole || '默认角色',
systemRole,
meta.title || t('noDescription'),
systemRole || t('defaultAgent'),
sessionSelectors.getAgentAvatar(meta),
meta.backgroundColor,
session?.updateAt,
@@ -70,42 +35,43 @@ const SessionItem: FC<SessionItemProps> = memo(({ id, active, simple = true, loa
}, shallow);
return (
<Flexbox className={styles.container} gap={4} paddingBlock={4}>
<List.Item
active={active}
avatar={
<Avatar
avatar={avatar}
background={avatarBackground}
shape="circle"
size={46}
title={title}
/>
}
className={styles.container}
classNames={{ time: cx('session-time', styles.time) }}
date={updateAt}
description={title}
loading={loading}
onClick={() => {
switchAgent(id);
}}
style={{
alignItems: 'center',
color: theme.colorText,
}}
title={systemRole}
>
<Popconfirm
arrow={false}
cancelText={t('cancel')}
okButtonProps={{ danger: true }}
okText={t('ok')}
onConfirm={() => removeSession(id)}
overlayStyle={{ width: 280 }}
placement={'right'}
title={'即将删除该会话,删除后该将无法找回,请确认你的操作。'}
title={t('confirmRemoveSessionItemAlert')}
>
<CloseOutlined className={cx(styles.closeCtn, active && styles.active)} />
<ActionIcon className="session-remove" icon={X} size={'small'} />
</Popconfirm>
<List.Item
active={active}
avatar={
<Avatar
avatar={avatar}
background={avatarBackground}
shape="circle"
size={46}
title={title}
/>
}
classNames={{ time: styles.time }}
date={updateAt}
description={simple ? undefined : systemRole}
loading={loading}
onClick={() => {
switchAgent(id);
}}
style={{
alignItems: 'center',
color: theme.colorText,
}}
title={title}
/>
</Flexbox>
</List.Item>
);
});

View File

@@ -1,49 +1,30 @@
import { createStyles } from 'antd-style';
import { rgba } from 'polished';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import { shallow } from 'zustand/shallow';
import { useStyles } from '@/pages/chat/SessionList/List/style';
import { sessionSelectors, useChatStore } from '@/store/session';
import SessionItem from './SessionItem';
export const useStyles = createStyles(({ css, token }) => ({
button: css`
position: sticky;
z-index: 30;
bottom: 0;
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
margin-top: 8px;
padding: 12px;
background: ${rgba(token.colorBgLayout, 0.5)};
backdrop-filter: blur(8px);
`,
}));
const SessionList = memo(() => {
const { styles, cx } = useStyles();
const [list, activeId, loading] = useChatStore(
(s) => [sessionSelectors.chatList(s), s.activeId, s.loading.summarizingTitle],
shallow,
);
return (
<>
<Flexbox className={cx(styles.list)}>
{list.map(({ id }) => (
<SessionItem
active={activeId === id}
id={id}
key={id}
loading={loading && id === activeId}
simple={false}
/>
))}
</>
</Flexbox>
);
});

View File

@@ -0,0 +1,77 @@
import { createStyles } from 'antd-style';
import { rgba } from 'polished';
export const useStyles = createStyles(({ css, token, cx, stylish }) => {
return {
active: css`
display: flex;
`,
button: css`
position: sticky;
z-index: 30;
bottom: 0;
display: flex;
gap: 8px;
align-items: center;
justify-content: center;
margin-top: 8px;
padding: 12px;
background: ${rgba(token.colorBgLayout, 0.5)};
backdrop-filter: blur(8px);
`,
container: css`
position: relative;
.session-remove {
position: absolute;
top: 50%;
right: 16px;
transform: translateY(-50%);
width: 16px;
height: 16px;
font-size: 10px;
opacity: 0;
background-color: ${token.colorFillTertiary};
transition: color 600ms ${token.motionEaseOut}, scale 400ms ${token.motionEaseOut},
background-color 100ms ${token.motionEaseOut}, opacity 100ms ${token.motionEaseOut};
&:hover {
background-color: ${token.colorFill};
}
}
.session-time {
opacity: 1;
transition: opacity 100ms ${token.motionEaseOut};
}
&:hover {
.session-time {
opacity: 0;
}
.session-remove {
opacity: 1;
}
}
`,
list: cx(
stylish.noScrollbar,
css`
overflow-x: hidden;
overflow-y: scroll;
`,
),
time: css`
align-self: flex-start;
`,
};
});

View File

@@ -1,4 +1,3 @@
import { createStyles } from 'antd-style';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
@@ -7,24 +6,12 @@ import FolderPanel from '@/features/FolderPanel';
import Header from './Header';
import SessionList from './List';
export const useStyles = createStyles(({ css }) => ({
center: css`
overflow-x: hidden;
overflow-y: scroll;
padding-inline: 4px 0;
`,
}));
export const Sessions = memo(() => {
const { styles, cx } = useStyles();
return (
<FolderPanel>
<Flexbox gap={8} height={'100%'}>
<Header />
<Flexbox className={cx(styles.center)}>
<SessionList />
</Flexbox>
<SessionList />
</Flexbox>
</FolderPanel>
);

View File

@@ -1,5 +1,5 @@
import { ActionIcon, Logo, SideNav } from '@lobehub/ui';
import { Album, MessageSquare, Settings2 } from 'lucide-react';
import { MessageSquare, Settings2, Sticker } from 'lucide-react';
import { memo } from 'react';
import { shallow } from 'zustand/shallow';
@@ -22,7 +22,7 @@ const Sidebar = memo(() => {
/>
<ActionIcon
active={tab === 'market'}
icon={Album}
icon={Sticker}
onClick={() => setTab('market')}
size="large"
/>

View File

@@ -1,5 +1,6 @@
import type { GetStaticProps, InferGetStaticPropsType } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { memo } from 'react';
import Chat from './chat/index.page';
@@ -14,4 +15,4 @@ export const getStaticProps: GetStaticProps = async ({ locale }) => ({
},
});
export default Index;
export default memo(Index);

View File

@@ -1,4 +1,3 @@
import { MetaData } from '@/types/meta';
export const getAgentAvatar = (s: MetaData) =>
s.avatar || 'https://npm.elemecdn.com/@lobehub/assets-logo/assets/logo-3d.webp';
export const getAgentAvatar = (s: MetaData) => s.avatar || '😎';

View File

@@ -0,0 +1,39 @@
import { Theme, css } from 'antd-style';
import { rgba } from 'polished';
export default (token: Theme) => css`
.ant-btn {
box-shadow: none;
}
.ant-btn-default:not(:disabled):not(.ant-btn-dangerous) {
border-color: transparent;
&:hover {
color: ${token.colorText};
background: ${token.isDarkMode ? token.colorFill : token.colorFillTertiary};
border-color: transparent;
}
}
.ant-popover {
z-index: 1100;
}
.ant-notification .ant-notification-notice.notification-primary-info {
background: ${token.colorPrimary};
box-shadow: 0 6px 16px 0 ${rgba(token.colorPrimary, 0.1)},
0 3px 6px -4px ${rgba(token.colorPrimary, 0.2)},
0 9px 28px 8px ${rgba(token.colorPrimary, 0.1)};
.anticon {
color: ${token.colorTextLightSolid};
}
.ant-notification-notice-message {
margin-bottom: 0;
padding-right: 0;
color: ${token.colorTextLightSolid};
}
}
`;

23
src/styles/global.ts Normal file
View File

@@ -0,0 +1,23 @@
import { css } from 'antd-style';
export default () => css`
body,
.ant-app {
::-webkit-scrollbar {
width: 0;
height: 0;
}
}
#__next {
height: 100%;
}
p {
margin-bottom: 0;
}
li {
display: block;
}
`;

6
src/styles/index.ts Normal file
View File

@@ -0,0 +1,6 @@
import { createGlobalStyle } from 'antd-style';
import antdOverride from './antdOverride';
import global from './global';
export const GlobalStyle = createGlobalStyle(({ theme }) => [global(), antdOverride(theme)]);

14
src/types/global.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
import type { LobeCustomStylish, LobeCustomToken } from '@lobehub/ui';
import 'antd-style';
import { AntdToken } from 'antd-style/lib/types/theme';
declare module 'antd-style' {
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface CustomToken extends LobeCustomToken {}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface CustomStylish extends LobeCustomStylish {}
}
declare module 'styled-components' {
export interface DefaultTheme extends AntdToken, LobeCustomToken {}
}