💄 style: Update topic list header

This commit is contained in:
canisminor1990
2023-11-04 18:26:38 +08:00
parent cd506f1879
commit ce932d7b11
35 changed files with 383 additions and 182 deletions
+8 -2
View File
@@ -22,7 +22,7 @@
"searchAgentPlaceholder": "Search agents and conversations...",
"send": "Send",
"sendPlaceholder": "Enter chat content...",
"sessionList": "Assistant List",
"sessionList": "Agent List",
"shareModal": {
"download": "Download Screenshot",
"imageType": "Image Format",
@@ -47,7 +47,13 @@
"defaultTitle": "Default Topic",
"saveCurrentMessages": "Save Current Session as Topic",
"searchPlaceholder": "Search topics...",
"title": "Topic"
"deleteAll": "Delete All Topics",
"deleteUnstarred": "Delete Unstarred Topics",
"title": "Topic List",
"confirmRemoveAll": "All topics will be deleted and cannot be recovered. Please proceed with caution.",
"confirmRemoveUnstarred": "All unstarred topics will be deleted and cannot be recovered. Please proceed with caution.",
"removeAll": "Remove All Topics",
"removeUnstarred": "Remove Unstarred Topics"
},
"translate": {
"clear": "Clear Translation"
+7 -1
View File
@@ -47,7 +47,13 @@
"defaultTitle": "デフォルトトピック",
"saveCurrentMessages": "現在の会話をトピックとして保存",
"searchPlaceholder": "トピックを検索...",
"title": "トピック"
"deleteAll": "すべてのトピックを削除する",
"deleteUnstarred": "スターを付けていないトピックを削除する",
"title": "トピックリスト",
"confirmRemoveAll": "すべてのトピックを削除します。削除後は元に戻すことはできませんので、注意して操作してください。",
"confirmRemoveUnstarred": "スターをつけていないトピックを削除します。削除後は元に戻すことはできませんので、注意して操作してください。",
"removeAll": "すべてのトピックを削除",
"removeUnstarred": "スターをつけていないトピックを削除"
},
"translate": {
"clear": "翻訳をクリア"
+7 -1
View File
@@ -47,7 +47,13 @@
"defaultTitle": "기본 주제",
"saveCurrentMessages": "현재 대화를 주제로 저장",
"searchPlaceholder": "주제 검색...",
"title": "주제"
"deleteAll": "모든 주제 삭제",
"deleteUnstarred": "스타가 없는 주제 삭제",
"title": "주제 목록",
"confirmRemoveAll": "모든 주제를 삭제하려고 합니다. 삭제 후에는 복구할 수 없으므로 신중하게 작업하십시오.",
"confirmRemoveUnstarred": "스타를 지정하지 않은 주제를 삭제하려고 합니다. 삭제 후에는 복구할 수 없으므로 신중하게 작업하십시오.",
"removeAll": "모든 주제 삭제",
"removeUnstarred": "스타를 지정하지 않은 주제 삭제"
},
"translate": {
"clear": "번역 지우기"
+7 -1
View File
@@ -47,7 +47,13 @@
"defaultTitle": "Стандартная тема",
"saveCurrentMessages": "Сохранить текущий разговор как тему",
"searchPlaceholder": "Поиск темы...",
"title": "Тема"
"deleteAll": "Удалить все темы",
"deleteUnstarred": "Удалить неотмеченные темы",
"title": "Список тем",
"confirmRemoveAll": "Вы собираетесь удалить все темы. После удаления их будет невозможно восстановить. Пожалуйста, будьте осторожны.",
"confirmRemoveUnstarred": "Вы собираетесь удалить неотмеченные темы. После удаления их будет невозможно восстановить. Пожалуйста, будьте осторожны.",
"removeAll": "Удалить все темы",
"removeUnstarred": "Удалить неотмеченные темы"
},
"translate": {
"clear": "Очистить перевод"
+5 -1
View File
@@ -43,11 +43,15 @@
"used": "使用"
},
"topic": {
"confirmRemoveAll": "即将删除全部话题,删除后将不可恢复,请谨慎操作。",
"confirmRemoveTopic": "即将删除该话题,删除后将不可恢复,请谨慎操作。",
"confirmRemoveUnstarred": "即将删除未收藏话题,删除后将不可恢复,请谨慎操作。",
"defaultTitle": "默认话题",
"removeAll": "删除全部话题",
"removeUnstarred": "删除未收藏话题",
"saveCurrentMessages": "将当前会话保存为话题",
"searchPlaceholder": "搜索话题...",
"title": "话题"
"title": "话题列表"
},
"translate": {
"clear": "删除翻译"
+7 -1
View File
@@ -47,7 +47,13 @@
"defaultTitle": "默認話題",
"saveCurrentMessages": "將當前會話保存為話題",
"searchPlaceholder": "搜索話題...",
"title": "話題"
"deleteAll": "刪除所有話題",
"deleteUnstarred": "刪除未收藏話題",
"title": "話題列表",
"confirmRemoveAll": "即將刪除全部話題,刪除後將無法恢復,請謹慎操作。",
"confirmRemoveUnstarred": "即將刪除未收藏話題,刪除後將無法恢復,請謹慎操作。",
"removeAll": "刪除全部話題",
"removeUnstarred": "刪除未收藏話題"
},
"translate": {
"clear": "刪除翻譯"
+1 -4
View File
@@ -27,7 +27,7 @@
"build:analyze": "ANALYZE=true next build",
"build:docker": "DOCKER=true next build",
"dev": "next dev -p 3010",
"i18n": "npm run i18n:workflow && lobe-i18n",
"i18n": "npm run workflow:i18n && lobe-i18n",
"lint": "npm run lint:ts && npm run lint:style && npm run type-check && npm run lint:circular",
"lint:circular": "dpdm src/**/*.ts --warning false --tree false --exit-code circular:1 -T true",
"lint:md": "remark . --quiet --frail --output",
@@ -67,8 +67,6 @@
},
"dependencies": {
"@ant-design/icons": "^5",
"@emoji-mart/data": "^1",
"@emoji-mart/react": "^1",
"@icons-pack/react-simple-icons": "^9",
"@lobehub/chat-plugin-sdk": "latest",
"@lobehub/chat-plugins-gateway": "latest",
@@ -82,7 +80,6 @@
"chroma-js": "^2",
"copy-to-clipboard": "^3",
"dayjs": "^1",
"emoji-mart": "^5",
"fast-deep-equal": "^3",
"gpt-tokenizer": "^2",
"i18next": "^23",
+11 -1
View File
@@ -55,7 +55,17 @@ const Header = memo(() => {
</Flexbox>
) : (
<Flexbox align={'flex-start'} gap={12} horizontal>
<Avatar avatar={avatar} background={backgroundColor} size={40} title={title} />
<Avatar
avatar={avatar}
background={backgroundColor}
onClick={() =>
isInbox
? router.push('/settings/agent')
: router.push(pathString('/chat/settings', { hash: location.hash }))
}
size={40}
title={title}
/>
<ChatHeaderTitle
desc={displayDesc}
tag={
@@ -14,7 +14,7 @@ const useStyles = createStyles(({ stylish, css, cx }) =>
display: flex;
flex-direction: column;
gap: 2px;
padding: 0 6px;
padding: 8px 8px 0;
`,
),
);
+1 -1
View File
@@ -11,7 +11,7 @@ import { sessionSelectors } from '@/store/session/selectors';
import TopicListContent from '../../features/TopicListContent';
const SystemRole = dynamic(() => import('../../features/TopicListContent/SystemRole'));
const SystemRole = dynamic(() => import('../../features/SystemRole'));
const useStyles = createStyles(({ css, token }) => ({
content: css`
@@ -1,5 +1,5 @@
import { ActionIcon, MobileNavBar, MobileNavBarTitle } from '@lobehub/ui';
import { LayoutList, Settings } from 'lucide-react';
import { Clock3, Settings } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -32,11 +32,7 @@ const MobileHeader = memo(() => {
right={
<>
<ShareButton />
<ActionIcon
icon={LayoutList}
onClick={() => toggleConfig()}
size={MOBILE_HEADER_ICON_SIZE}
/>
<ActionIcon icon={Clock3} onClick={() => toggleConfig()} size={MOBILE_HEADER_ICON_SIZE} />
{!isInbox && (
<ActionIcon
icon={Settings}
+1 -1
View File
@@ -16,7 +16,7 @@ const Topics = memo(() => {
return (
<Modal onCancel={() => toggleConfig(false)} open={showAgentSettings} title={t('topic.title')}>
<TopicListContent />
<TopicListContent mobile />
</Modal>
);
});
@@ -1,5 +1,7 @@
import { ChatListProps } from '@lobehub/ui';
import { useSessionStore } from '@/store/session';
import { AssistantActionsBar } from './Assistant';
import { DefaultActionsBar } from './Fallback';
import { FunctionActionsBar } from './Function';
@@ -11,3 +13,49 @@ export const renderActions: ChatListProps['renderActions'] = {
system: DefaultActionsBar,
user: UserActionsBar,
};
interface ActionsClick {
onClick: () => void;
trigger: boolean;
}
export const useActionsClick = (): ChatListProps['onActionsClick'] => {
const [deleteMessage, resendMessage, translateMessage] = useSessionStore((s) => [
s.deleteMessage,
s.resendMessage,
s.translateMessage,
]);
return (action, { id, error }) => {
const actionsClick: ActionsClick[] = [
{
onClick: () => {
deleteMessage(id);
},
trigger: action.key === 'del',
},
{
onClick: () => {
resendMessage(id);
// if this message is an error message, we need to delete it
if (error) deleteMessage(id);
},
trigger: action.key === 'regenerate',
},
{
onClick: () => {
/**
* @description Click the menu item with translate item, the result is:
* @key 'en-US'
* @keyPath ['en-US','translate']
*/
const lang = action.keyPath[0];
translateMessage(id, lang);
},
trigger: action.keyPath.at(-1) === 'translate',
},
];
actionsClick.find((item) => item.trigger)?.onClick();
};
};
@@ -1,4 +1,11 @@
import { ChatListProps } from '@lobehub/ui';
import { useResponsive } from 'antd-style';
import { useRouter } from 'next/navigation';
import { useGlobalStore } from '@/store/global';
import { useSessionStore } from '@/store/session';
import { sessionSelectors } from '@/store/session/slices/session/selectors';
import { pathString } from '@/utils/url';
import { AssistantMessage } from './Assistant';
import { DefaultMessage } from './Default';
@@ -9,3 +16,23 @@ export const renderMessages: ChatListProps['renderMessages'] = {
default: DefaultMessage,
function: FunctionMessage,
};
export const useAvatarsClick = (): ChatListProps['onAvatarsClick'] => {
const [isInbox] = useSessionStore((s) => [sessionSelectors.isInboxSession(s)]);
const [toggleSystemRole] = useGlobalStore((s) => [s.toggleSystemRole]);
const { mobile } = useResponsive();
const router = useRouter();
return (role) => {
switch (role) {
case 'assistant': {
return () =>
isInbox
? router.push('/settings/agent')
: mobile
? router.push(pathString('/chat/settings', { hash: location.hash }))
: toggleSystemRole(true);
}
}
};
};
@@ -8,15 +8,16 @@ import { PREFIX_KEY, REGENERATE_KEY } from '@/const/hotkeys';
import { useSessionChatInit, useSessionStore } from '@/store/session';
import { agentSelectors, chatSelectors } from '@/store/session/selectors';
import { renderActions } from './Actions';
import { renderActions, useActionsClick } from './Actions';
import { renderErrorMessages } from './Error';
import { renderMessagesExtra } from './Extras';
import { renderMessages } from './Messages';
import { renderMessages, useAvatarsClick } from './Messages';
import SkeletonList from './SkeletonList';
const List = memo(() => {
const init = useSessionChatInit();
const { t } = useTranslation('common');
const data = useSessionStore(chatSelectors.currentChatsWithGuideMessage, isEqual);
const [
@@ -24,10 +25,8 @@ const List = memo(() => {
enableHistoryCount,
historyCount,
chatLoadingId,
deleteMessage,
resendMessage,
dispatchMessage,
translateMessage,
] = useSessionStore((s) => {
const config = agentSelectors.currentAgentConfig(s);
return [
@@ -35,12 +34,12 @@ const List = memo(() => {
config.enableHistoryCount,
config.historyCount,
s.chatLoadingId,
s.deleteMessage,
s.resendMessage,
s.dispatchMessage,
s.translateMessage,
];
});
const onActionsClick = useActionsClick();
const onAvatarsClick = useAvatarsClick();
const hotkeys = [PREFIX_KEY, REGENERATE_KEY].join('+');
@@ -64,29 +63,8 @@ const List = memo(() => {
enableHistoryCount={enableHistoryCount}
historyCount={historyCount}
loadingId={chatLoadingId}
onActionsClick={(action, { id, error }) => {
switch (action.key) {
case 'del': {
deleteMessage(id);
break;
}
case 'regenerate': {
resendMessage(id);
// if this message is an error message, we need to delete it
if (error) deleteMessage(id);
break;
}
}
// click the menu item with translate item, the result is:
// key: 'en-US'
// keyPath: ['en-US','translate']
if (action.keyPath.at(-1) === 'translate') {
const lang = action.keyPath[0];
translateMessage(id, lang);
}
}}
onActionsClick={onActionsClick}
onAvatarsClick={onAvatarsClick}
onMessageChange={(id, content) =>
dispatchMessage({ id, key: 'content', type: 'updateMessage', value: content })
}
@@ -9,13 +9,12 @@ const useStyles = createStyles(({ css, token }) => ({
`,
}));
interface HeaderProps {
interface SidebarHeaderProps {
actions?: ReactNode;
mobile?: boolean;
title: string;
title: ReactNode;
}
const Header = memo<HeaderProps>(({ title, actions }) => {
const SidebarHeader = memo<SidebarHeaderProps>(({ title, actions }) => {
const { styles } = useStyles();
return (
@@ -24,15 +23,17 @@ const Header = memo<HeaderProps>(({ title, actions }) => {
className={styles.header}
distribution={'space-between'}
horizontal
padding={12}
padding={14}
paddingInline={16}
>
<Flexbox>{title}</Flexbox>
<Flexbox gap={4} horizontal>
<Flexbox align={'center'} gap={4} horizontal>
{title}
</Flexbox>
<Flexbox align={'center'} gap={2} horizontal>
{actions}
</Flexbox>
</Flexbox>
);
});
export default Header;
export default SidebarHeader;
@@ -1,19 +1,23 @@
import { ActionIcon, EditableMessage } from '@lobehub/ui';
import { Skeleton } from 'antd';
import { Edit } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import useMergeState from 'use-merge-value';
import AgentInfo from '@/features/AgentInfo';
import { useGlobalStore } from '@/store/global';
import { useSessionChatInit, useSessionStore } from '@/store/session';
import { agentSelectors } from '@/store/session/selectors';
import { pathString } from '@/utils/url';
import Header from './Header';
import SidebarHeader from '../SidebarHeader';
import { useStyles } from './style';
const SystemRole = memo(() => {
const [openModal, setOpenModal] = useState(false);
const router = useRouter();
const [editing, setEditing] = useState(false);
const { styles } = useStyles();
const [systemRole, meta, updateAgentConfig] = useSessionStore((s) => [
@@ -21,30 +25,45 @@ const SystemRole = memo(() => {
agentSelectors.currentAgentMeta(s),
s.updateAgentConfig,
]);
const [showSystemRole, toggleSystemRole] = useGlobalStore((s) => [
s.preference.showSystemRole,
s.toggleSystemRole,
]);
const [open, setOpen] = useMergeState(false, {
defaultValue: showSystemRole,
onChange: toggleSystemRole,
value: showSystemRole,
});
const init = useSessionChatInit();
const { t } = useTranslation('common');
const handleOpenWithEdit = () => {
if (!init) return;
setEditing(true);
setOpen(true);
};
const handleOpen = () => {
if (!init) return;
setEditing(false);
setOpen(true);
};
return (
<Flexbox height={'fit-content'}>
<Header
<SidebarHeader
actions={
<ActionIcon
icon={Edit}
onClick={() => setOpenModal(true)}
size={'small'}
title={t('edit')}
/>
<ActionIcon icon={Edit} onClick={handleOpenWithEdit} size={'small'} title={t('edit')} />
}
title={t('settingAgent.prompt.title', { ns: 'setting' })}
/>
<Flexbox
className={styles.promptBox}
height={200}
onClick={handleOpen}
onDoubleClick={(e) => {
if (e.altKey) {
setOpenModal(true);
setEditing(true);
}
if (e.altKey) handleOpenWithEdit();
}}
>
{!init ? (
@@ -59,13 +78,23 @@ const SystemRole = memo(() => {
<EditableMessage
classNames={{ markdown: styles.prompt }}
editing={editing}
model={{ extra: <AgentInfo meta={meta} style={{ marginBottom: 16 }} /> }}
model={{
extra: (
<AgentInfo
meta={meta}
onAvatarClick={() =>
router.push(pathString('/chat/settings', { hash: location.hash }))
}
style={{ marginBottom: 16 }}
/>
),
}}
onChange={(e) => {
updateAgentConfig({ systemRole: e });
}}
onEditingChange={setEditing}
onOpenChange={setOpenModal}
openModal={openModal}
onOpenChange={setOpen}
openModal={open}
placeholder={`${t('settingAgent.prompt.placeholder', { ns: 'setting' })}...`}
styles={{ markdown: systemRole ? {} : { opacity: 0.5 } }}
text={{
@@ -0,0 +1,86 @@
import { ActionIcon, Icon } from '@lobehub/ui';
import { App, Dropdown, MenuProps } from 'antd';
import { MoreHorizontal, Search, Trash } from 'lucide-react';
import { memo, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import { useSessionStore } from '@/store/session';
import { topicSelectors } from '@/store/session/selectors';
import SidebarHeader from '../SidebarHeader';
import TopicSearchBar from './TopicSearchBar';
const Header = memo(() => {
const { t } = useTranslation('chat');
const [topicLength, removeUnstarredTopic, removeAllTopic] = useSessionStore((s) => [
topicSelectors.currentTopicLength(s),
s.removeUnstarredTopic,
s.removeAllTopic,
]);
const [showSearch, setShowSearch] = useState(false);
const { modal } = App.useApp();
const items = useMemo<MenuProps['items']>(
() => [
{
icon: <Icon icon={Trash} />,
key: 'deleteUnstarred',
label: t('topic.removeUnstarred'),
onClick: () => {
modal.confirm({
centered: true,
okButtonProps: { danger: true },
onOk: removeUnstarredTopic,
title: t('topic.confirmRemoveUnstarred', { ns: 'chat' }),
});
},
},
{
danger: true,
icon: <Icon icon={Trash} />,
key: 'deleteAll',
label: t('topic.removeAll'),
onClick: () => {
modal.confirm({
centered: true,
okButtonProps: { danger: true },
onOk: removeAllTopic,
title: t('topic.confirmRemoveAll', { ns: 'chat' }),
});
},
},
],
[],
);
return showSearch ? (
<Flexbox padding={'12px 16px 4px'}>
<TopicSearchBar onClear={() => setShowSearch(false)} />
</Flexbox>
) : (
<SidebarHeader
actions={
<>
<ActionIcon icon={Search} onClick={() => setShowSearch(true)} size={'small'} />
<Dropdown
arrow={false}
menu={{
items: items,
onClick: ({ domEvent }) => {
domEvent.stopPropagation();
},
}}
trigger={['click']}
>
<ActionIcon icon={MoreHorizontal} size={'small'} />
</Dropdown>
</>
}
title={`${t('topic.title')} ${topicLength > 1 ? topicLength + 1 : ''}`}
/>
);
});
export default Header;
@@ -4,7 +4,7 @@ import { Flexbox } from 'react-layout-kit';
const SkeletonList = memo(() => {
return (
<Flexbox gap={8} style={{ marginTop: 8 }}>
<Flexbox gap={8} paddingInline={10} style={{ marginTop: 8 }}>
{Array.from({ length: 8 }).map((_, i) => (
<Skeleton
active
@@ -5,14 +5,19 @@ import { useTranslation } from 'react-i18next';
import { useSessionStore } from '@/store/session';
const TopicSearchBar = memo(() => {
const TopicSearchBar = memo<{ onClear?: () => void }>(({ onClear }) => {
const { t } = useTranslation('chat');
const [keywords] = useSessionStore((s) => [s.topicSearchKeywords]);
const { mobile } = useResponsive();
return (
<SearchBar
allowClear
onChange={(e) => useSessionStore.setState({ topicSearchKeywords: e.target.value })}
autoFocus
onBlur={() => {
if (keywords === '') onClear?.();
}}
onChange={(e) => {
useSessionStore.setState({ topicSearchKeywords: e.target.value });
}}
placeholder={t('topic.searchPlaceholder')}
spotlight={!mobile}
type={mobile ? 'block' : 'ghost'}
@@ -1,19 +1,30 @@
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import { Topic } from './Topic';
import TopicSearchBar from './TopicSearchBar';
import TopicSearchBar from '@/app/chat/features/TopicListContent/TopicSearchBar';
const TopicListContent = () => {
import Header from './Header';
import { Topic } from './Topic';
const TopicListContent = memo<{ mobile?: boolean }>(({ mobile }) => {
return (
<Flexbox height={'100%'} style={{ overflow: 'hidden' }}>
<Flexbox padding={16}>
<TopicSearchBar />
</Flexbox>
<Flexbox gap={16} paddingInline={16} style={{ overflowY: 'auto', position: 'relative' }}>
{mobile ? (
<Flexbox padding={'12px 16px'}>
<TopicSearchBar />
</Flexbox>
) : (
<Header />
)}
<Flexbox
gap={16}
paddingInline={mobile ? 16 : 8}
style={{ overflowY: 'auto', paddingTop: 6, position: 'relative' }}
>
<Topic />
</Flexbox>
</Flexbox>
);
};
});
export default TopicListContent;
+1 -1
View File
@@ -31,7 +31,7 @@ const SideBar = memo(() => {
</div>
</Flexbox>
<UpgradeAlert />
<Flexbox gap={2} style={{ paddingInline: 6 }}>
<Flexbox gap={2} style={{ paddingInline: 8 }}>
<List />
</Flexbox>
</DraggablePanelBody>
-56
View File
@@ -1,56 +0,0 @@
import data from '@emoji-mart/data';
import i18n from '@emoji-mart/data/i18n/zh.json';
import Picker from '@emoji-mart/react';
import { Avatar } from '@lobehub/ui';
import { Popover } from 'antd';
import { memo } from 'react';
import useMergeState from 'use-merge-value';
import { DEFAULT_AVATAR, DEFAULT_BACKGROUND_COLOR } from '@/const/meta';
import { useStyles } from './style';
export interface EmojiPickerProps {
backgroundColor?: string;
defaultAvatar?: string;
onChange?: (emoji: string) => void;
value?: string;
}
const EmojiPicker = memo<EmojiPickerProps>(
({ value, defaultAvatar, backgroundColor = DEFAULT_BACKGROUND_COLOR, onChange }) => {
const { styles } = useStyles();
const [ava, setAva] = useMergeState(DEFAULT_AVATAR, {
defaultValue: defaultAvatar,
onChange,
value,
});
return (
<Popover
content={
<div className={styles.picker}>
<Picker
data={data}
i18n={i18n}
locale={'zh'}
onEmojiSelect={(e: any) => setAva(e.native)}
skinTonePosition={'none'}
theme={'auto'}
/>
</div>
}
placement={'left'}
rootClassName={styles.popover}
trigger={'click'}
>
<div className={styles.avatar} style={{ width: 'fit-content' }}>
<Avatar avatar={ava} background={backgroundColor} size={44} />
</div>
</Popover>
);
},
);
export default EmojiPicker;
-30
View File
@@ -1,30 +0,0 @@
import { createStyles } from 'antd-style';
import chroma from 'chroma-js';
export const useStyles = createStyles(({ css, token, prefixCls }) => ({
avatar: css`
border-radius: 50%;
transition:
scale 400ms ${token.motionEaseOut},
box-shadow 100ms ${token.motionEaseOut};
&:hover {
box-shadow: 0 0 0 3px ${token.colorText};
}
&:active {
scale: 0.8;
}
`,
picker: css`
em-emoji-picker {
--rgb-accent: ${chroma(token.colorPrimary) .rgb() .join(',')};
--shadow: none;
}
`,
popover: css`
.${prefixCls}-popover-inner {
padding: 0;
}
`,
}));
+3 -1
View File
@@ -26,11 +26,12 @@ const useStyles = createStyles(({ css, token, stylish }) => ({
export interface AgentInfoProps {
meta?: MetaData;
onAvatarClick?: () => void;
style?: CSSProperties;
systemRole?: string;
}
const AgentInfo = memo<AgentInfoProps>(({ systemRole, style, meta }) => {
const AgentInfo = memo<AgentInfoProps>(({ systemRole, style, meta, onAvatarClick }) => {
const { styles, theme } = useStyles();
if (!meta) return;
@@ -43,6 +44,7 @@ const AgentInfo = memo<AgentInfoProps>(({ systemRole, style, meta }) => {
avatar={meta.avatar}
background={meta.backgroundColor || theme.colorFillTertiary}
className={styles.avatar}
onClick={onAvatarClick}
size={100}
/>
)}
@@ -8,6 +8,7 @@ import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { FORM_STYLE } from '@/const/layoutTokens';
import { settingsSelectors, useGlobalStore } from '@/store/global';
import { useStore } from '../store';
import { SessionLoadingState } from '../store/initialState';
@@ -15,16 +16,18 @@ import AutoGenerateInput from './AutoGenerateInput';
import AutoGenerateSelect from './AutoGenerateSelect';
import BackgroundSwatches from './BackgroundSwatches';
const EmojiPicker = dynamic(() => import('@/components/EmojiPicker'), { ssr: false });
const EmojiPicker = dynamic(() => import('@lobehub/ui/es/EmojiPicker'), { ssr: false });
const AgentMeta = memo(() => {
const { t } = useTranslation('setting');
const [hasSystemRole, updateMeta, autocompleteMeta, autocompleteAllMeta] = useStore((s) => [
!!s.config.systemRole,
s.setAgentMeta,
s.autocompleteMeta,
s.autocompleteAllMeta,
]);
const locale = useGlobalStore(settingsSelectors.currentLanguage);
const loading = useStore((s) => s.autocompleteLoading);
const meta = useStore((s) => s.meta, isEqual);
@@ -77,6 +80,7 @@ const AgentMeta = memo(() => {
children: (
<EmojiPicker
backgroundColor={meta.backgroundColor}
locale={locale}
onChange={(avatar) => updateMeta({ avatar })}
value={meta.avatar}
/>
+4 -3
View File
@@ -4,13 +4,14 @@ import dynamic from 'next/dynamic';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { settingsSelectors, useGlobalStore } from '@/store/global';
import { pluginSelectors, usePluginStore } from '@/store/plugin';
const EmojiPicker = dynamic(() => import('@/components/EmojiPicker'), { ssr: false });
const EmojiPicker = dynamic(() => import('@lobehub/ui/es/EmojiPicker'), { ssr: false });
const MetaForm = memo<{ form: FormInstance; mode?: 'edit' | 'create' }>(({ form, mode }) => {
const isEditMode = mode === 'edit';
const locale = useGlobalStore(settingsSelectors.currentLanguage);
const { t } = useTranslation('plugin');
const [plugins] = usePluginStore((s) => [pluginSelectors.pluginList(s).map((i) => i.identifier)]);
@@ -65,7 +66,7 @@ const MetaForm = memo<{ form: FormInstance; mode?: 'edit' | 'create' }>(({ form,
name: 'homepage',
},
{
children: <EmojiPicker defaultAvatar={'🧩'} />,
children: <EmojiPicker defaultAvatar={'🧩'} locale={locale} />,
desc: t('dev.meta.avatar.desc'),
label: t('dev.meta.avatar.label'),
name: ['meta', 'avatar'],
+12 -2
View File
@@ -1,5 +1,5 @@
import { ConfigProvider } from 'antd';
import Zh_CN from 'antd/locale/zh_CN';
import defaultLocale from 'antd/locale/en_US';
import { PropsWithChildren, memo, useState } from 'react';
import { createI18nNext } from '@/locales/create';
@@ -12,6 +12,7 @@ interface LocaleLayoutProps extends PropsWithChildren {
}
const InnerLocale = memo<LocaleLayoutProps>(({ children, lang }) => {
const [locale, setLocale] = useState(defaultLocale);
const [i18n] = useState(createI18nNext(lang));
// if run on server side, init i18n instance everytime
@@ -26,13 +27,22 @@ const InnerLocale = memo<LocaleLayoutProps>(({ children, lang }) => {
});
}
const getLocale = async (localeName: string) => {
setLocale((await import(`antd/locale/${localeName}.js`)) as any);
};
useOnFinishHydrationGlobal((s) => {
if (s.settings.language === 'auto') {
switchLang('auto');
}
if (lang?.includes('-') && lang !== 'en-US') {
const localeName = lang?.replace('-', '_');
getLocale(localeName);
}
}, []);
return <ConfigProvider locale={Zh_CN}>{children}</ConfigProvider>;
return <ConfigProvider locale={locale}>{children}</ConfigProvider>;
});
// const Locale = memo<LocaleLayoutProps>((props) => (
+5 -1
View File
@@ -46,11 +46,15 @@ export default {
used: '使用',
},
topic: {
confirmRemoveAll: '即将删除全部话题,删除后将不可恢复,请谨慎操作。',
confirmRemoveTopic: '即将删除该话题,删除后将不可恢复,请谨慎操作。',
confirmRemoveUnstarred: '即将删除未收藏话题,删除后将不可恢复,请谨慎操作。',
defaultTitle: '默认话题',
removeAll: '删除全部话题',
removeUnstarred: '删除未收藏话题',
saveCurrentMessages: '将当前会话保存为话题',
searchPlaceholder: '搜索话题...',
title: '话题',
title: '话题列表',
},
translate: {
clear: '删除翻译',
+2
View File
@@ -26,6 +26,7 @@ export interface GlobalPreference {
sessionsWidth: number;
showChatSideBar?: boolean;
showSessionPanel?: boolean;
showSystemRole?: boolean;
}
export interface GlobalState {
@@ -54,6 +55,7 @@ export const initialState: GlobalState = {
sessionsWidth: 320,
showChatSideBar: true,
showSessionPanel: true,
showSystemRole: false,
},
settings: DEFAULT_SETTINGS,
settingsTab: SettingsTabs.Common,
+7
View File
@@ -24,6 +24,7 @@ export interface CommonAction {
switchSideBar: (key: SidebarTabKey) => void;
toggleChatSideBar: (visible?: boolean) => void;
toggleMobileTopic: (visible?: boolean) => void;
toggleSystemRole: (visible?: boolean) => void;
updateGuideState: (guide: Partial<Guide>) => void;
updatePreference: (preference: Partial<GlobalPreference>, action?: string) => void;
useCheckLatestVersion: () => SWRResponse<string>;
@@ -50,6 +51,12 @@ export const createCommonSlice: StateCreator<
get().updatePreference({ mobileShowTopic }, t('toggleMobileTopic', newValue) as string);
},
toggleSystemRole: (newValue) => {
const showSystemRole =
typeof newValue === 'boolean' ? newValue : !get().preference.mobileShowTopic;
get().updatePreference({ showSystemRole }, t('toggleMobileTopic', newValue) as string);
},
updateGuideState: (guide) => {
const { updatePreference } = get();
const nextGuide = merge(get().preference.guide, guide);
@@ -18,11 +18,19 @@ export interface ChatTopicAction {
* @param payload - 要分发的主题
*/
dispatchTopic: (payload: ChatTopicDispatch) => void;
/**
* 移出所有话题
*/
removeAllTopic: () => void;
/**
* 移除话题
* @param id
*/
removeTopic: (id: string) => void;
/**
* 移出所有未标记的话题
*/
removeUnstarredTopic: () => void;
/**
* 将当前消息保存为主题
*/
@@ -54,6 +62,17 @@ export const chatTopic: StateCreator<
get().dispatchSession({ id: activeId, topics, type: 'updateSessionTopic' });
},
removeAllTopic: () => {
const { removeTopic, toggleTopic } = get();
const topics = topicSelectors.currentTopics(get());
for (const { id } of topics) {
removeTopic(id);
}
// 切换到默认 topic
toggleTopic();
},
removeTopic: (id) => {
const { dispatchTopic, dispatchMessage, toggleTopic } = get();
@@ -72,6 +91,17 @@ export const chatTopic: StateCreator<
// 切换到默认 topic
toggleTopic();
},
removeUnstarredTopic: () => {
const { removeTopic, toggleTopic } = get();
const topics = topicSelectors.currentTopics(get());
for (const { id, favorite } of topics) {
if (!favorite) removeTopic(id);
}
// 切换到默认 topic
toggleTopic();
},
saveToTopic: async () => {
const session = sessionSelectors.currentSession(get());
if (!session) return;
@@ -7,7 +7,7 @@ import {
getFunctionMessageProps,
getMessageById,
} from './chat';
import { currentTopics, getTopicMessages } from './topic';
import { currentTopicLength, currentTopics, getTopicMessages } from './topic';
export const chatSelectors = {
chatsMessageString,
@@ -20,6 +20,7 @@ export const chatSelectors = {
};
export const topicSelectors = {
currentTopicLength,
currentTopics,
getTopicMessages,
};
@@ -18,6 +18,10 @@ export const currentTopics = (s: SessionStore): ChatTopic[] => {
return [...favTopics, ...defaultTopics];
};
export const currentTopicLength = (s: SessionStore): number => {
return currentTopics(s).length;
};
export const getTopicMessages = (topicId: string) => (s: SessionStore) => {
const session = sessionSelectors.currentSession(s);
if (!session) return [];