mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-27 13:29:15 +07:00
✨ feat: 实现 Topic 重命名功能
This commit is contained in:
@@ -53,6 +53,7 @@ export default {
|
||||
tokenDetail: '系统设定: {{systemRoleToken}} 历史消息: {{chatsToken}}',
|
||||
topic: {
|
||||
confirmRemoveTopic: '即将删除该话题,删除后将不可恢复,请谨慎操作。',
|
||||
defaultTitle: '默认话题',
|
||||
saveCurrentMessages: '将当前会话保存为话题',
|
||||
searchPlaceholder: '搜索话题...',
|
||||
},
|
||||
|
||||
@@ -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`
|
||||
|
||||
29
src/pages/chat/[id]/Config/Topic/DefaultContent.tsx
Normal file
29
src/pages/chat/[id]/Config/Topic/DefaultContent.tsx
Normal 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;
|
||||
151
src/pages/chat/[id]/Config/Topic/TopicContent.tsx
Normal file
151
src/pages/chat/[id]/Config/Topic/TopicContent.tsx
Normal 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;
|
||||
70
src/pages/chat/[id]/Config/Topic/TopicItem.tsx
Normal file
70
src/pages/chat/[id]/Config/Topic/TopicItem.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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;
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export interface ChatState {
|
||||
activeTopicId?: string;
|
||||
chatLoadingId?: string;
|
||||
renameTopicId?: string;
|
||||
topicLoadingId?: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user