feat: 实现 Topic 重命名功能

This commit is contained in:
arvinxx
2023-07-30 19:56:00 +08:00
parent c241c4bb66
commit 5ef1685b6c
9 changed files with 254 additions and 167 deletions

View File

@@ -53,6 +53,7 @@ export default {
tokenDetail: '系统设定: {{systemRoleToken}} 历史消息: {{chatsToken}}',
topic: {
confirmRemoveTopic: '即将删除该话题,删除后将不可恢复,请谨慎操作。',
defaultTitle: '默认话题',
saveCurrentMessages: '将当前会话保存为话题',
searchPlaceholder: '搜索话题...',
},

View File

@@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import { shallow } from 'zustand/shallow';
import { Topic } from '@/pages/chat/[id]/Config/Topic';
import { agentSelectors, useSessionStore } from '@/store/session';
import Header from './Header';
import { Topic } from './Topic';
const useStyles = createStyles(({ css, token }) => ({
desc: css`

View File

@@ -0,0 +1,29 @@
import { Icon } from '@lobehub/ui';
import { Tag, Typography } from 'antd';
import { useTheme } from 'antd-style';
import { MessageSquareDashed } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
const { Paragraph } = Typography;
const DefaultContent = memo(() => {
const { t } = useTranslation('common');
const theme = useTheme();
return (
<Flexbox align={'center'} gap={8} horizontal>
<Flexbox align={'center'} height={24} justify={'center'} width={24}>
<Icon color={theme.colorTextDescription} icon={MessageSquareDashed} />
</Flexbox>
<Paragraph ellipsis={{ rows: 1 }} style={{ margin: 0 }}>
{t('topic.defaultTitle')}
</Paragraph>
<Tag>{t('temp')}</Tag>
</Flexbox>
);
});
export default DefaultContent;

View File

@@ -0,0 +1,151 @@
import { ActionIcon, EditableText, Icon } from '@lobehub/ui';
import { App, Dropdown, type MenuProps, Typography } from 'antd';
import { createStyles } from 'antd-style';
import { MoreVertical, PencilLine, Star, Trash } from 'lucide-react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { shallow } from 'zustand/shallow';
import { useSessionStore } from '@/store/session';
const useStyles = createStyles(({ css }) => ({
content: css`
position: relative;
overflow: hidden;
flex: 1;
`,
title: css`
flex: 1;
height: 28px;
line-height: 28px;
text-align: start;
`,
}));
const { Paragraph } = Typography;
interface TopicContentProps {
fav?: boolean;
id: string;
title: string;
}
const TopicContent = memo<TopicContentProps>(({ id, title, fav }) => {
const { t } = useTranslation('common');
const [editing, dispatchTopic, removeTopic] = useSessionStore(
(s) => [s.renameTopicId === id, s.dispatchTopic, s.removeTopic],
shallow,
);
const { styles, theme } = useStyles();
const toggleEditing = (visible?: boolean) => {
useSessionStore.setState({ renameTopicId: visible ? id : '' });
};
const { modal } = App.useApp();
const items = useMemo<MenuProps['items']>(
() => [
{
icon: <Icon icon={PencilLine} />,
key: 'rename',
label: t('rename'),
onClick: () => {
toggleEditing(true);
},
},
// {
// icon: <Icon icon={Share2} />,
// key: 'share',
// label: t('share'),
// },
{
danger: true,
icon: <Icon icon={Trash} />,
key: 'delete',
label: t('delete'),
onClick: () => {
if (!id) return;
modal.confirm({
centered: true,
okButtonProps: { danger: true },
onOk: () => {
removeTopic(id);
},
title: t('topic.confirmRemoveTopic'),
});
},
},
],
[],
);
return (
<>
<ActionIcon
color={fav ? theme.colorWarning : undefined}
fill={fav ? theme.colorWarning : 'transparent'}
icon={Star}
onClick={() => {
if (!id) return;
dispatchTopic({ favorite: !fav, id, type: 'favorChatTopic' });
}}
size={'small'}
/>
{!editing ? (
<Paragraph
className={styles.title}
ellipsis={{ rows: 1, tooltip: title }}
style={{ margin: 0 }}
>
{title}
</Paragraph>
) : (
<EditableText
editing={editing}
onChangeEnd={(v) => {
if (title !== v) {
dispatchTopic({ id, key: 'title', type: 'updateChatTopic', value: v });
}
toggleEditing(false);
}}
onEditingChange={toggleEditing}
showEditIcon={false}
size={'small'}
style={{
background: theme.colorBgContainer,
borderRadius: 6,
height: 28,
padding: '0 2px',
}}
value={title}
/>
)}
{!editing && (
<Dropdown
arrow={false}
menu={{
items: items,
onClick: ({ domEvent }) => {
domEvent.stopPropagation();
},
}}
trigger={['click']}
>
<ActionIcon
className="topic-more"
icon={MoreVertical}
onClick={(e) => {
e.stopPropagation();
}}
size={'small'}
/>
</Dropdown>
)}
</>
);
});
export default TopicContent;

View File

@@ -0,0 +1,70 @@
import { createStyles } from 'antd-style';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import { shallow } from 'zustand/shallow';
import { useSessionStore } from '@/store/session';
import DefaultContent from './DefaultContent';
import TopicContent from './TopicContent';
const useStyles = createStyles(({ css, token, isDarkMode }) => ({
active: css`
background: ${isDarkMode ? token.colorFillTertiary : token.colorFillSecondary};
transition: background 200ms ${token.motionEaseOut};
&:hover {
background: ${token.colorFill};
}
`,
container: css`
cursor: pointer;
padding: 12px 8px;
border-radius: ${token.borderRadius}px;
.topic-more {
opacity: 0;
transition: opacity 400ms ${token.motionEaseOut};
}
&:hover {
background: ${token.colorFillSecondary};
.topic-more {
opacity: 1;
}
}
`,
split: css`
border-bottom: 1px solid ${token.colorSplit};
`,
}));
export interface ConfigCellProps {
active?: boolean;
fav?: boolean;
id?: string;
title: string;
}
const TopicItem = memo<ConfigCellProps>(({ title, active, id, fav }) => {
const { styles, cx } = useStyles();
const [toggleTopic] = useSessionStore((s) => [s.toggleTopic], shallow);
return (
<Flexbox
align={'center'}
className={cx(styles.container, active && styles.active)}
distribution={'space-between'}
horizontal
onClick={() => {
toggleTopic(id);
}}
>
{!id ? <DefaultContent /> : <TopicContent fav={fav} id={id} title={title} />}
</Flexbox>
);
});
export default TopicItem;

View File

@@ -13,14 +13,7 @@ export const Topic = () => {
<TopicItem active={!activeTopicId} fav={false} title={'默认话题'} />
{topics.map(({ id, favorite, title }) => (
<TopicItem
active={activeTopicId === id}
fav={favorite}
id={id}
key={id}
showFav
title={title}
/>
<TopicItem active={activeTopicId === id} fav={favorite} id={id} key={id} title={title} />
))}
</Flexbox>
);

View File

@@ -1,156 +0,0 @@
import { ActionIcon, Icon } from '@lobehub/ui';
import { App, Dropdown, type MenuProps, Tag, Typography } from 'antd';
import { createStyles } from 'antd-style';
import { MessageSquareDashed, MoreVertical, PencilLine, Share2, Star, Trash } from 'lucide-react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import { shallow } from 'zustand/shallow';
import { useSessionStore } from '@/store/session';
const { Paragraph } = Typography;
const useStyles = createStyles(({ css, token, isDarkMode }) => ({
active: css`
background: ${isDarkMode ? token.colorFillTertiary : token.colorFillSecondary};
transition: background 200ms ${token.motionEaseOut};
&:hover {
background: ${token.colorFill};
}
`,
container: css`
cursor: pointer;
padding: 12px 8px;
border-radius: ${token.borderRadius}px;
.topic-more {
opacity: 0;
transition: opacity 400ms ${token.motionEaseOut};
}
&:hover {
background: ${token.colorFillSecondary};
.topic-more {
opacity: 1;
}
}
`,
split: css`
border-bottom: 1px solid ${token.colorSplit};
`,
}));
export interface ConfigCellProps {
active?: boolean;
fav?: boolean;
id?: string;
showFav?: boolean;
title: string;
}
const TopicItem = memo<ConfigCellProps>(({ title, active, id, showFav, fav }) => {
const { styles, theme, cx } = useStyles();
const { t } = useTranslation('common');
const [dispatchTopic, toggleTopic, removeTopic] = useSessionStore(
(s) => [s.dispatchTopic, s.toggleTopic, s.removeTopic],
shallow,
);
const { modal } = App.useApp();
// TODO: 动作绑定
const items = useMemo<MenuProps['items']>(
() => [
{
icon: <Icon icon={PencilLine} />,
key: 'rename',
label: t('rename'),
},
{
icon: <Icon icon={Share2} />,
key: 'share',
label: t('share'),
},
{
danger: true,
icon: <Icon icon={Trash} />,
key: 'delete',
label: t('delete'),
onClick: () => {
if (!id) return;
modal.confirm({
centered: true,
okButtonProps: { danger: true },
onOk: () => {
removeTopic(id);
},
title: t('topic.confirmRemoveTopic'),
});
},
},
],
[],
);
return (
<Flexbox
align={'center'}
className={cx(styles.container, active && styles.active)}
distribution={'space-between'}
horizontal
onClick={() => {
toggleTopic(id);
}}
>
<Flexbox align={'center'} gap={8} horizontal>
{!showFav ? (
<Flexbox align={'center'} height={24} justify={'center'} width={24}>
<Icon color={theme.colorTextDescription} icon={MessageSquareDashed} />
</Flexbox>
) : (
<ActionIcon
color={fav ? theme.colorWarning : undefined}
fill={fav ? theme.colorWarning : 'transparent'}
icon={Star}
onClick={() => {
if (!id) return;
dispatchTopic({ favorite: !fav, id, type: 'favorChatTopic' });
}}
size={'small'}
/>
)}
<Paragraph ellipsis={{ rows: 2 }} style={{ margin: 0 }}>
{title}
</Paragraph>
{id ? '' : <Tag>{t('temp')}</Tag>}
</Flexbox>
{!id ? null : (
<Dropdown
arrow={false}
menu={{
items,
onClick: ({ domEvent }) => {
domEvent.stopPropagation();
},
}}
trigger={['click']}
>
<ActionIcon
className="topic-more"
icon={MoreVertical}
onClick={(e) => {
e.stopPropagation();
}}
size={'small'}
/>
</Dropdown>
)}
</Flexbox>
);
});
export default TopicItem;

View File

@@ -32,11 +32,9 @@ const Config = () => {
<DraggablePanel
className={styles.drawer}
expand={showAgentSettings}
maxWidth={CHAT_SIDEBAR_WIDTH}
minWidth={CHAT_SIDEBAR_WIDTH}
onExpandChange={toggleConfig}
placement={'right'}
resize={false}
>
<HeaderSpacing />
<DraggablePanelContainer style={{ flex: 'none', minWidth: CHAT_SIDEBAR_WIDTH }}>

View File

@@ -1,6 +1,7 @@
export interface ChatState {
activeTopicId?: string;
chatLoadingId?: string;
renameTopicId?: string;
topicLoadingId?: string;
}