mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
💄 style: Update topic list header
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -47,7 +47,13 @@
|
||||
"defaultTitle": "デフォルトトピック",
|
||||
"saveCurrentMessages": "現在の会話をトピックとして保存",
|
||||
"searchPlaceholder": "トピックを検索...",
|
||||
"title": "トピック"
|
||||
"deleteAll": "すべてのトピックを削除する",
|
||||
"deleteUnstarred": "スターを付けていないトピックを削除する",
|
||||
"title": "トピックリスト",
|
||||
"confirmRemoveAll": "すべてのトピックを削除します。削除後は元に戻すことはできませんので、注意して操作してください。",
|
||||
"confirmRemoveUnstarred": "スターをつけていないトピックを削除します。削除後は元に戻すことはできませんので、注意して操作してください。",
|
||||
"removeAll": "すべてのトピックを削除",
|
||||
"removeUnstarred": "スターをつけていないトピックを削除"
|
||||
},
|
||||
"translate": {
|
||||
"clear": "翻訳をクリア"
|
||||
|
||||
@@ -47,7 +47,13 @@
|
||||
"defaultTitle": "기본 주제",
|
||||
"saveCurrentMessages": "현재 대화를 주제로 저장",
|
||||
"searchPlaceholder": "주제 검색...",
|
||||
"title": "주제"
|
||||
"deleteAll": "모든 주제 삭제",
|
||||
"deleteUnstarred": "스타가 없는 주제 삭제",
|
||||
"title": "주제 목록",
|
||||
"confirmRemoveAll": "모든 주제를 삭제하려고 합니다. 삭제 후에는 복구할 수 없으므로 신중하게 작업하십시오.",
|
||||
"confirmRemoveUnstarred": "스타를 지정하지 않은 주제를 삭제하려고 합니다. 삭제 후에는 복구할 수 없으므로 신중하게 작업하십시오.",
|
||||
"removeAll": "모든 주제 삭제",
|
||||
"removeUnstarred": "스타를 지정하지 않은 주제 삭제"
|
||||
},
|
||||
"translate": {
|
||||
"clear": "번역 지우기"
|
||||
|
||||
@@ -47,7 +47,13 @@
|
||||
"defaultTitle": "Стандартная тема",
|
||||
"saveCurrentMessages": "Сохранить текущий разговор как тему",
|
||||
"searchPlaceholder": "Поиск темы...",
|
||||
"title": "Тема"
|
||||
"deleteAll": "Удалить все темы",
|
||||
"deleteUnstarred": "Удалить неотмеченные темы",
|
||||
"title": "Список тем",
|
||||
"confirmRemoveAll": "Вы собираетесь удалить все темы. После удаления их будет невозможно восстановить. Пожалуйста, будьте осторожны.",
|
||||
"confirmRemoveUnstarred": "Вы собираетесь удалить неотмеченные темы. После удаления их будет невозможно восстановить. Пожалуйста, будьте осторожны.",
|
||||
"removeAll": "Удалить все темы",
|
||||
"removeUnstarred": "Удалить неотмеченные темы"
|
||||
},
|
||||
"translate": {
|
||||
"clear": "Очистить перевод"
|
||||
|
||||
@@ -43,11 +43,15 @@
|
||||
"used": "使用"
|
||||
},
|
||||
"topic": {
|
||||
"confirmRemoveAll": "即将删除全部话题,删除后将不可恢复,请谨慎操作。",
|
||||
"confirmRemoveTopic": "即将删除该话题,删除后将不可恢复,请谨慎操作。",
|
||||
"confirmRemoveUnstarred": "即将删除未收藏话题,删除后将不可恢复,请谨慎操作。",
|
||||
"defaultTitle": "默认话题",
|
||||
"removeAll": "删除全部话题",
|
||||
"removeUnstarred": "删除未收藏话题",
|
||||
"saveCurrentMessages": "将当前会话保存为话题",
|
||||
"searchPlaceholder": "搜索话题...",
|
||||
"title": "话题"
|
||||
"title": "话题列表"
|
||||
},
|
||||
"translate": {
|
||||
"clear": "删除翻译"
|
||||
|
||||
@@ -47,7 +47,13 @@
|
||||
"defaultTitle": "默認話題",
|
||||
"saveCurrentMessages": "將當前會話保存為話題",
|
||||
"searchPlaceholder": "搜索話題...",
|
||||
"title": "話題"
|
||||
"deleteAll": "刪除所有話題",
|
||||
"deleteUnstarred": "刪除未收藏話題",
|
||||
"title": "話題列表",
|
||||
"confirmRemoveAll": "即將刪除全部話題,刪除後將無法恢復,請謹慎操作。",
|
||||
"confirmRemoveUnstarred": "即將刪除未收藏話題,刪除後將無法恢復,請謹慎操作。",
|
||||
"removeAll": "刪除全部話題",
|
||||
"removeUnstarred": "刪除未收藏話題"
|
||||
},
|
||||
"translate": {
|
||||
"clear": "刪除翻譯"
|
||||
|
||||
+1
-4
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
`,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
-8
@@ -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;
|
||||
+45
-16
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
`,
|
||||
}));
|
||||
@@ -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,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'],
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -46,11 +46,15 @@ export default {
|
||||
used: '使用',
|
||||
},
|
||||
topic: {
|
||||
confirmRemoveAll: '即将删除全部话题,删除后将不可恢复,请谨慎操作。',
|
||||
confirmRemoveTopic: '即将删除该话题,删除后将不可恢复,请谨慎操作。',
|
||||
confirmRemoveUnstarred: '即将删除未收藏话题,删除后将不可恢复,请谨慎操作。',
|
||||
defaultTitle: '默认话题',
|
||||
removeAll: '删除全部话题',
|
||||
removeUnstarred: '删除未收藏话题',
|
||||
saveCurrentMessages: '将当前会话保存为话题',
|
||||
searchPlaceholder: '搜索话题...',
|
||||
title: '话题',
|
||||
title: '话题列表',
|
||||
},
|
||||
translate: {
|
||||
clear: '删除翻译',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
Reference in New Issue
Block a user