mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
Compare commits
6 Commits
dev
...
style/sync
| Author | SHA1 | Date | |
|---|---|---|---|
| 3aa8be4a13 | |||
| 4d573ab697 | |||
| f83b99197e | |||
| 6a2961dd4a | |||
| 33ad7e490f | |||
| dde83026b8 |
@@ -1,19 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import RawConversation from '@/features/Conversation';
|
||||
|
||||
import TelemetryNotification from '../../features/TelemetryNotification';
|
||||
import ChatInput from './ChatInput';
|
||||
import HotKeys from './HotKeys';
|
||||
|
||||
const Conversation = memo(() => {
|
||||
return (
|
||||
<>
|
||||
<RawConversation chatInput={<ChatInput />} />
|
||||
<HotKeys />
|
||||
<TelemetryNotification />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Conversation;
|
||||
@@ -1,22 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import ClientResponsiveContent from '@/components/client/ClientResponsiveContent';
|
||||
|
||||
import ChatHeader from './features/ChatHeader';
|
||||
import Conversation from './features/Conversation';
|
||||
import SideBar from './features/SideBar';
|
||||
|
||||
const Desktop = memo(() => (
|
||||
<>
|
||||
<ChatHeader />
|
||||
<Flexbox flex={1} height={'calc(100% - 64px)'} horizontal>
|
||||
<Conversation />
|
||||
<SideBar />
|
||||
</Flexbox>
|
||||
</>
|
||||
));
|
||||
|
||||
export default ClientResponsiveContent({ Desktop, Mobile: () => import('../(mobile)') });
|
||||
@@ -1,57 +0,0 @@
|
||||
import { ActionIcon, Avatar, Logo, MobileNavBar } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { MessageSquarePlus } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
|
||||
import SyncStatusInspector from '@/features/SyncStatusInspector';
|
||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { userProfileSelectors } from '@/store/user/selectors';
|
||||
import { mobileHeaderSticky } from '@/styles/mobileHeader';
|
||||
|
||||
export const useStyles = createStyles(({ css, token }) => ({
|
||||
logo: css`
|
||||
fill: ${token.colorText};
|
||||
`,
|
||||
top: css`
|
||||
position: sticky;
|
||||
top: 0;
|
||||
`,
|
||||
}));
|
||||
|
||||
const Header = memo(() => {
|
||||
const [createSession] = useSessionStore((s) => [s.createSession]);
|
||||
const router = useRouter();
|
||||
const avatar = useUserStore(userProfileSelectors.userAvatar);
|
||||
const { showCreateSession } = useServerConfigStore(featureFlagsSelectors);
|
||||
|
||||
return (
|
||||
<MobileNavBar
|
||||
left={
|
||||
<Flexbox align={'center'} gap={8} horizontal style={{ marginLeft: 8 }}>
|
||||
<div onClick={() => router.push('/me')}>
|
||||
{avatar ? <Avatar avatar={avatar} size={28} /> : <Logo size={28} />}
|
||||
</div>
|
||||
<Logo type={'text'} />
|
||||
<SyncStatusInspector placement={'bottom'} />
|
||||
</Flexbox>
|
||||
}
|
||||
right={
|
||||
showCreateSession && (
|
||||
<ActionIcon
|
||||
icon={MessageSquarePlus}
|
||||
onClick={() => createSession()}
|
||||
size={MOBILE_HEADER_ICON_SIZE}
|
||||
/>
|
||||
)
|
||||
}
|
||||
style={mobileHeaderSticky}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default Header;
|
||||
@@ -1,17 +0,0 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import SessionListContent from '../../features/SessionListContent';
|
||||
import SessionSearchBar from '../../features/SessionSearchBar';
|
||||
|
||||
const Sessions = memo(() => {
|
||||
return (
|
||||
<>
|
||||
<div style={{ padding: '8px 16px' }}>
|
||||
<SessionSearchBar mobile />
|
||||
</div>
|
||||
<SessionListContent />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default Sessions;
|
||||
@@ -1,26 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { memo, useEffect } from 'react';
|
||||
|
||||
import MobileContentLayout from '@/components/server/MobileNavLayout';
|
||||
|
||||
import SessionHeader from './features/SessionHeader';
|
||||
import SessionList from './features/SessionList';
|
||||
|
||||
const ChatMobilePage = memo(() => {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
router.prefetch('/chat/mobile');
|
||||
router.prefetch('/settings');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MobileContentLayout header={<SessionHeader />} withNav>
|
||||
<SessionList />
|
||||
</MobileContentLayout>
|
||||
);
|
||||
});
|
||||
|
||||
export default ChatMobilePage;
|
||||
@@ -1,26 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { memo } from 'react';
|
||||
|
||||
import MobileContentLayout from '@/components/server/MobileNavLayout';
|
||||
import Conversation from '@/features/Conversation';
|
||||
|
||||
import SessionHydration from '../../components/SessionHydration';
|
||||
import TelemetryNotification from '../../features/TelemetryNotification';
|
||||
import ChatInput from '../features/ChatInput';
|
||||
import ChatHeader from './ChatHeader';
|
||||
|
||||
const TopicList = dynamic(() => import('../features/TopicList'));
|
||||
|
||||
const Chat = memo(() => {
|
||||
return (
|
||||
<MobileContentLayout header={<ChatHeader />}>
|
||||
<Conversation chatInput={<ChatInput />} mobile />
|
||||
<TopicList />
|
||||
<TelemetryNotification mobile />
|
||||
<SessionHydration />
|
||||
</MobileContentLayout>
|
||||
);
|
||||
});
|
||||
export default Chat;
|
||||
@@ -0,0 +1,16 @@
|
||||
import Conversation from '@/features/Conversation';
|
||||
import { isMobileDevice } from '@/utils/responsive';
|
||||
|
||||
import DesktopChatInput from './features/ChatInput/Desktop';
|
||||
import MobileChatInput from './features/ChatInput/Mobile';
|
||||
|
||||
const ChatConversation = () => {
|
||||
const mobile = isMobileDevice();
|
||||
const ChatInput = mobile ? MobileChatInput : DesktopChatInput;
|
||||
|
||||
return <Conversation chatInput={<ChatInput />} mobile={mobile} />;
|
||||
};
|
||||
|
||||
ChatConversation.displayName = 'ChatConversation';
|
||||
|
||||
export default ChatConversation;
|
||||
+6
-2
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { DraggablePanel } from '@lobehub/ui';
|
||||
import { memo, useState } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
@@ -13,7 +15,7 @@ import Footer from './Footer';
|
||||
import Head from './Header';
|
||||
import TextArea from './TextArea';
|
||||
|
||||
const ChatInput = memo(() => {
|
||||
const DesktopChatInput = memo(() => {
|
||||
const [expand, setExpand] = useState<boolean>(false);
|
||||
|
||||
const [inputHeight, updatePreference] = useGlobalStore((s) => [
|
||||
@@ -52,4 +54,6 @@ const ChatInput = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
export default ChatInput;
|
||||
DesktopChatInput.displayName = 'DesktopChatInput';
|
||||
|
||||
export default DesktopChatInput;
|
||||
+7
-2
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { MobileChatInputArea, MobileChatSendButton } from '@lobehub/ui';
|
||||
import { useTheme } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
@@ -10,7 +12,7 @@ import { useChatInput } from '@/features/ChatInput/useChatInput';
|
||||
|
||||
import Files from './Files';
|
||||
|
||||
const ChatInputMobileLayout = memo(() => {
|
||||
const MobileChatInput = memo(() => {
|
||||
const { t } = useTranslation('chat');
|
||||
const theme = useTheme();
|
||||
const { ref, onSend, loading, value, onInput, onStop, expand, setExpand } = useChatInput();
|
||||
@@ -27,6 +29,7 @@ const ChatInputMobileLayout = memo(() => {
|
||||
setExpand={setExpand}
|
||||
style={{
|
||||
background: `linear-gradient(to bottom, ${theme.colorFillQuaternary}, transparent)`,
|
||||
top: expand ? 0 : undefined,
|
||||
width: '100%',
|
||||
}}
|
||||
textAreaLeftAddons={<STT mobile />}
|
||||
@@ -44,4 +47,6 @@ const ChatInputMobileLayout = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
export default ChatInputMobileLayout;
|
||||
MobileChatInput.displayName = 'MobileChatInput';
|
||||
|
||||
export default MobileChatInput;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { isMobileDevice } from '@/utils/responsive';
|
||||
|
||||
import SystemRole from './features/SystemRole';
|
||||
import TopicListContent from './features/TopicListContent';
|
||||
|
||||
const Topic = () => {
|
||||
const mobile = isMobileDevice();
|
||||
|
||||
return (
|
||||
<>
|
||||
{!mobile && <SystemRole />}
|
||||
<TopicListContent mobile={mobile} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Topic.displayName = 'ChatTopic';
|
||||
|
||||
export default Topic;
|
||||
@@ -0,0 +1,39 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
import { type ReactNode, memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
header: css`
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 6px ${token.colorBgLayout};
|
||||
`,
|
||||
}));
|
||||
|
||||
interface SidebarHeaderProps {
|
||||
actions?: ReactNode;
|
||||
title: ReactNode;
|
||||
}
|
||||
|
||||
const SidebarHeader = memo<SidebarHeaderProps>(({ title, actions }) => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={styles.header}
|
||||
distribution={'space-between'}
|
||||
horizontal
|
||||
padding={14}
|
||||
paddingInline={16}
|
||||
>
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
{title}
|
||||
</Flexbox>
|
||||
<Flexbox align={'center'} gap={2} horizontal>
|
||||
{actions}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default SidebarHeader;
|
||||
+4
-2
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { ActionIcon, EditableMessage } from '@lobehub/ui';
|
||||
import { Skeleton } from 'antd';
|
||||
import { Edit } from 'lucide-react';
|
||||
@@ -21,7 +23,7 @@ import { useStyles } from './style';
|
||||
const SystemRole = memo(() => {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const { styles } = useStyles();
|
||||
const openChatSettings = useOpenChatSettings(ChatSettingsTabs.Prompt);
|
||||
const openChatSettings = useOpenChatSettings();
|
||||
const [init, meta] = useSessionStore((s) => [
|
||||
sessionSelectors.isSomeSessionActive(s),
|
||||
sessionMetaSelectors.currentAgentMeta(s),
|
||||
@@ -92,7 +94,7 @@ const SystemRole = memo(() => {
|
||||
onAvatarClick={() => {
|
||||
setOpen(false);
|
||||
setEditing(false);
|
||||
openChatSettings();
|
||||
openChatSettings(ChatSettingsTabs.Prompt);
|
||||
}}
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
@@ -0,0 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
|
||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { sessionSelectors } from '@/store/session/selectors';
|
||||
|
||||
import SystemRoleContent from './SystemRoleContent';
|
||||
|
||||
const SystemRole = memo(() => {
|
||||
const { isAgentEditable: showSystemRole } = useServerConfigStore(featureFlagsSelectors);
|
||||
const isInbox = useSessionStore(sessionSelectors.isInboxSession);
|
||||
|
||||
return showSystemRole && !isInbox && <SystemRoleContent />;
|
||||
});
|
||||
|
||||
export default SystemRole;
|
||||
@@ -0,0 +1,88 @@
|
||||
'use client';
|
||||
|
||||
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 { useChatStore } from '@/store/chat';
|
||||
import { topicSelectors } from '@/store/chat/selectors';
|
||||
|
||||
import SidebarHeader from '../SidebarHeader';
|
||||
import TopicSearchBar from './TopicSearchBar';
|
||||
|
||||
const Header = memo(() => {
|
||||
const { t } = useTranslation('chat');
|
||||
const [topicLength, removeUnstarredTopic, removeAllTopic] = useChatStore((s) => [
|
||||
topicSelectors.currentTopicLength(s),
|
||||
s.removeUnstarredTopic,
|
||||
s.removeSessionTopics,
|
||||
]);
|
||||
|
||||
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;
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { Icon, Tag } from '@lobehub/ui';
|
||||
import { 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', { ns: 'chat' })}
|
||||
</Paragraph>
|
||||
<Tag>{t('temp')}</Tag>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default DefaultContent;
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { Skeleton } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
const useStyles = createStyles(({ css, prefixCls }) => ({
|
||||
container: css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
height: 44px;
|
||||
padding: 8px;
|
||||
|
||||
.${prefixCls}-skeleton-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
`,
|
||||
|
||||
paragraph: css`
|
||||
> li {
|
||||
height: 24px !important;
|
||||
}
|
||||
`,
|
||||
}));
|
||||
|
||||
export const Placeholder = memo(() => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
active
|
||||
avatar={false}
|
||||
className={styles.container}
|
||||
paragraph={{
|
||||
className: styles.paragraph,
|
||||
rows: 1,
|
||||
style: { marginBottom: 0 },
|
||||
width: '100%',
|
||||
}}
|
||||
title={false}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const SkeletonList = memo(() => (
|
||||
<Flexbox>
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<Placeholder key={i} />
|
||||
))}
|
||||
</Flexbox>
|
||||
));
|
||||
|
||||
export default SkeletonList;
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
import { ActionIcon, EditableText, Icon } from '@lobehub/ui';
|
||||
import { App, Dropdown, type MenuProps, Typography } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import {
|
||||
LucideCopy,
|
||||
LucideLoader2,
|
||||
MoreVertical,
|
||||
PencilLine,
|
||||
Star,
|
||||
Trash,
|
||||
Wand2,
|
||||
} from 'lucide-react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useChatStore } from '@/store/chat';
|
||||
|
||||
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;
|
||||
showMore?: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const [
|
||||
editing,
|
||||
favoriteTopic,
|
||||
updateTopicTitle,
|
||||
removeTopic,
|
||||
autoRenameTopicTitle,
|
||||
duplicateTopic,
|
||||
isLoading,
|
||||
] = useChatStore((s) => [
|
||||
s.topicRenamingId === id,
|
||||
s.favoriteTopic,
|
||||
s.updateTopicTitle,
|
||||
s.removeTopic,
|
||||
s.autoRenameTopicTitle,
|
||||
s.duplicateTopic,
|
||||
s.topicLoadingIds.includes(id),
|
||||
]);
|
||||
const { styles, theme } = useStyles();
|
||||
|
||||
const toggleEditing = (visible?: boolean) => {
|
||||
useChatStore.setState({ topicRenamingId: visible ? id : '' });
|
||||
};
|
||||
|
||||
const { modal } = App.useApp();
|
||||
|
||||
const items = useMemo<MenuProps['items']>(
|
||||
() => [
|
||||
{
|
||||
icon: <Icon icon={Wand2} />,
|
||||
key: 'autoRename',
|
||||
label: t('topic.actions.autoRename', { ns: 'chat' }),
|
||||
onClick: () => {
|
||||
autoRenameTopicTitle(id);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={PencilLine} />,
|
||||
key: 'rename',
|
||||
label: t('rename'),
|
||||
onClick: () => {
|
||||
toggleEditing(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={LucideCopy} />,
|
||||
key: 'duplicate',
|
||||
label: t('topic.actions.duplicate', { ns: 'chat' }),
|
||||
onClick: () => {
|
||||
duplicateTopic(id);
|
||||
},
|
||||
},
|
||||
// {
|
||||
// icon: <Icon icon={LucideDownload} />,
|
||||
// key: 'export',
|
||||
// label: t('topic.actions.export', { ns: 'chat' }),
|
||||
// onClick: () => {
|
||||
// configService.exportSingleTopic(sessionId, id);
|
||||
// },
|
||||
// },
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
// {
|
||||
// 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: async () => {
|
||||
await removeTopic(id);
|
||||
},
|
||||
title: t('topic.confirmRemoveTopic', { ns: 'chat' }),
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
gap={8}
|
||||
horizontal
|
||||
justify={'space-between'}
|
||||
onDoubleClick={(e) => {
|
||||
if (!id) return;
|
||||
if (e.altKey) toggleEditing(true);
|
||||
}}
|
||||
>
|
||||
<ActionIcon
|
||||
color={fav && !isLoading ? theme.colorWarning : undefined}
|
||||
fill={fav && !isLoading ? theme.colorWarning : 'transparent'}
|
||||
icon={isLoading ? LucideLoader2 : Star}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!id) return;
|
||||
favoriteTopic(id, !fav);
|
||||
}}
|
||||
size={'small'}
|
||||
spin={isLoading}
|
||||
/>
|
||||
{!editing ? (
|
||||
<Paragraph
|
||||
className={styles.title}
|
||||
ellipsis={{ rows: 1, tooltip: { placement: 'left', title } }}
|
||||
style={{ margin: 0 }}
|
||||
>
|
||||
{title}
|
||||
</Paragraph>
|
||||
) : (
|
||||
<EditableText
|
||||
editing={editing}
|
||||
onChangeEnd={(v) => {
|
||||
if (title !== v) {
|
||||
updateTopicTitle(id, v);
|
||||
}
|
||||
toggleEditing(false);
|
||||
}}
|
||||
onEditingChange={toggleEditing}
|
||||
showEditIcon={false}
|
||||
size={'small'}
|
||||
style={{
|
||||
height: 28,
|
||||
}}
|
||||
type={'pure'}
|
||||
value={title}
|
||||
/>
|
||||
)}
|
||||
{showMore && !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>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default TopicContent;
|
||||
@@ -0,0 +1,73 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
import { memo, useState } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
|
||||
import DefaultContent from './DefaultContent';
|
||||
import TopicContent from './TopicContent';
|
||||
|
||||
const useStyles = createStyles(({ css, token, isDarkMode }) => ({
|
||||
active: css`
|
||||
background: ${isDarkMode ? token.colorFillSecondary : token.colorFillTertiary};
|
||||
transition: background 200ms ${token.motionEaseOut};
|
||||
|
||||
&:hover {
|
||||
background: ${token.colorFill};
|
||||
}
|
||||
`,
|
||||
container: css`
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: ${token.borderRadius}px;
|
||||
|
||||
&:hover {
|
||||
background: ${token.colorFillSecondary};
|
||||
}
|
||||
`,
|
||||
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 toggleConfig = useGlobalStore((s) => s.toggleMobileTopic);
|
||||
const [toggleTopic] = useChatStore((s) => [s.switchTopic]);
|
||||
const [isHover, setHovering] = useState(false);
|
||||
|
||||
return (
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
className={cx(styles.container, active && styles.active)}
|
||||
distribution={'space-between'}
|
||||
horizontal
|
||||
onClick={() => {
|
||||
toggleTopic(id);
|
||||
toggleConfig(false);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
setHovering(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHovering(false);
|
||||
}}
|
||||
>
|
||||
{!id ? (
|
||||
<DefaultContent />
|
||||
) : (
|
||||
<TopicContent fav={fav} id={id} showMore={isHover} title={title} />
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default TopicItem;
|
||||
@@ -0,0 +1,105 @@
|
||||
'use client';
|
||||
|
||||
import { EmptyCard } from '@lobehub/ui';
|
||||
import { css, cx, useThemeMode } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import React, { memo, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
|
||||
import { imageUrl } from '@/const/url';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { topicSelectors } from '@/store/chat/selectors';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { ChatTopic } from '@/types/topic';
|
||||
|
||||
import { Placeholder, SkeletonList } from './SkeletonList';
|
||||
import TopicItem from './TopicItem';
|
||||
|
||||
const container = css`
|
||||
> div {
|
||||
padding-inline: 8px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const Topic = memo(() => {
|
||||
const { t } = useTranslation('chat');
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const [topicsInit, activeTopicId, topicLength] = useChatStore((s) => [
|
||||
s.topicsInit,
|
||||
s.activeTopicId,
|
||||
topicSelectors.currentTopicLength(s),
|
||||
]);
|
||||
const [visible, updateGuideState] = useUserStore((s) => [
|
||||
s.preference.guide?.topic,
|
||||
s.updateGuideState,
|
||||
]);
|
||||
|
||||
const topics = useChatStore(
|
||||
(s) => [
|
||||
{
|
||||
favorite: false,
|
||||
id: 'default',
|
||||
title: t('topic.defaultTitle'),
|
||||
} as ChatTopic,
|
||||
...topicSelectors.displayTopics(s),
|
||||
],
|
||||
isEqual,
|
||||
);
|
||||
|
||||
const itemContent = useCallback(
|
||||
(index: number, { id, favorite, title }: ChatTopic) =>
|
||||
index === 0 ? (
|
||||
<TopicItem active={!activeTopicId} fav={favorite} title={title} />
|
||||
) : (
|
||||
<TopicItem active={activeTopicId === id} fav={favorite} id={id} key={id} title={title} />
|
||||
),
|
||||
[activeTopicId],
|
||||
);
|
||||
|
||||
const activeIndex = topics.findIndex((topic) => topic.id === activeTopicId);
|
||||
|
||||
return !topicsInit ? (
|
||||
<SkeletonList />
|
||||
) : (
|
||||
<Flexbox gap={2} height={'100%'} style={{ marginBottom: 12 }}>
|
||||
{topicLength === 0 && (
|
||||
<Flexbox flex={1} paddingInline={8}>
|
||||
<EmptyCard
|
||||
alt={t('topic.guide.desc')}
|
||||
cover={imageUrl(`empty_topic_${isDarkMode ? 'dark' : 'light'}.webp`)}
|
||||
desc={t('topic.guide.desc')}
|
||||
height={120}
|
||||
imageProps={{
|
||||
priority: true,
|
||||
}}
|
||||
onVisibleChange={(visible) => {
|
||||
updateGuideState({ topic: visible });
|
||||
}}
|
||||
style={{ marginBottom: 6 }}
|
||||
title={t('topic.guide.title')}
|
||||
visible={visible}
|
||||
width={200}
|
||||
/>
|
||||
</Flexbox>
|
||||
)}
|
||||
<Virtuoso
|
||||
className={cx(container)}
|
||||
components={{ ScrollSeekPlaceholder: Placeholder }}
|
||||
computeItemKey={(_, item) => item.id}
|
||||
data={topics}
|
||||
fixedItemHeight={44}
|
||||
initialTopMostItemIndex={Math.max(activeIndex, 0)}
|
||||
itemContent={itemContent}
|
||||
overscan={44 * 10}
|
||||
ref={virtuosoRef}
|
||||
scrollSeekConfiguration={{
|
||||
enter: (velocity) => Math.abs(velocity) > 350,
|
||||
exit: (velocity) => Math.abs(velocity) < 10,
|
||||
}}
|
||||
/>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { SearchBar } from '@lobehub/ui';
|
||||
import { useUnmount } from 'ahooks';
|
||||
import { useResponsive } from 'antd-style';
|
||||
import { memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useChatStore } from '@/store/chat';
|
||||
|
||||
const TopicSearchBar = memo<{ onClear?: () => void }>(({ onClear }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
|
||||
const [keywords, setKeywords] = useState('');
|
||||
const { mobile } = useResponsive();
|
||||
const [activeSessionId, useSearchTopics] = useChatStore((s) => [s.activeId, s.useSearchTopics]);
|
||||
|
||||
useSearchTopics(keywords, activeSessionId);
|
||||
useUnmount(() => {
|
||||
useChatStore.setState({ isSearchingTopic: false });
|
||||
});
|
||||
return (
|
||||
<SearchBar
|
||||
autoFocus
|
||||
onBlur={() => {
|
||||
if (keywords === '') onClear?.();
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
|
||||
setKeywords(value);
|
||||
useChatStore.setState({ isSearchingTopic: !!value });
|
||||
}}
|
||||
placeholder={t('topic.searchPlaceholder')}
|
||||
spotlight={!mobile}
|
||||
type={mobile ? 'block' : 'ghost'}
|
||||
value={keywords}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default TopicSearchBar;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import Header from './Header';
|
||||
import { Topic } from './Topic';
|
||||
import TopicSearchBar from './TopicSearchBar';
|
||||
|
||||
const TopicListContent = ({ mobile }: { mobile?: boolean }) => {
|
||||
return (
|
||||
<Flexbox gap={mobile ? 8 : 0} height={'100%'} style={{ overflow: 'hidden' }}>
|
||||
{mobile ? <TopicSearchBar /> : <Header />}
|
||||
<Flexbox gap={16} height={'100%'} style={{ paddingTop: 6, position: 'relative' }}>
|
||||
<Topic />
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopicListContent;
|
||||
+2
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { ActionIcon } from '@lobehub/ui';
|
||||
import { PanelRightClose, PanelRightOpen } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
+3
-1
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { Avatar, ChatHeaderTitle } from '@lobehub/ui';
|
||||
import { Skeleton } from 'antd';
|
||||
import { memo } from 'react';
|
||||
@@ -41,7 +43,7 @@ const Main = memo(() => {
|
||||
<Avatar
|
||||
avatar={avatar}
|
||||
background={backgroundColor}
|
||||
onClick={openChatSettings}
|
||||
onClick={() => openChatSettings()}
|
||||
size={40}
|
||||
title={title}
|
||||
/>
|
||||
+1
-1
@@ -19,7 +19,7 @@ const TitleTags = memo(() => {
|
||||
const showPlugin = useUserStore(modelProviderSelectors.isModelEnabledFunctionCall(model));
|
||||
|
||||
return (
|
||||
<Flexbox gap={8} horizontal>
|
||||
<Flexbox align={'center'} horizontal>
|
||||
<ModelSwitchPanel>
|
||||
<ModelTag model={model} />
|
||||
</ModelSwitchPanel>
|
||||
+1
-2
@@ -1,9 +1,8 @@
|
||||
import { ChatHeader } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
|
||||
import HeaderAction from './HeaderAction';
|
||||
import Main from './Main';
|
||||
|
||||
const Header = memo(() => <ChatHeader left={<Main />} right={<HeaderAction />} />);
|
||||
const Header = () => <ChatHeader left={<Main />} right={<HeaderAction />} />;
|
||||
|
||||
export default Header;
|
||||
+2
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
+11
-16
@@ -1,18 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { DraggablePanel, DraggablePanelContainer } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { memo } from 'react';
|
||||
import { createStyles, useResponsive } from 'antd-style';
|
||||
import { PropsWithChildren, memo, useLayoutEffect } from 'react';
|
||||
|
||||
import SafeSpacing from '@/components/SafeSpacing';
|
||||
import { CHAT_SIDEBAR_WIDTH } from '@/const/layoutTokens';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { sessionSelectors } from '@/store/session/selectors';
|
||||
|
||||
import TopicListContent from '../../../features/TopicListContent';
|
||||
|
||||
const SystemRole = dynamic(() => import('./SystemRole'));
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
content: css`
|
||||
@@ -29,15 +23,17 @@ const useStyles = createStyles(({ css, token }) => ({
|
||||
`,
|
||||
}));
|
||||
|
||||
const Desktop = memo(() => {
|
||||
const Desktop = memo(({ children }: PropsWithChildren) => {
|
||||
const { styles } = useStyles();
|
||||
const { md = true, lg = true } = useResponsive();
|
||||
const [showAgentSettings, toggleConfig] = useGlobalStore((s) => [
|
||||
s.preference.showChatSideBar,
|
||||
s.toggleChatSideBar,
|
||||
]);
|
||||
|
||||
const { isAgentEditable: showSystemRole } = useServerConfigStore(featureFlagsSelectors);
|
||||
const isInbox = useSessionStore(sessionSelectors.isInboxSession);
|
||||
useLayoutEffect(() => {
|
||||
toggleConfig(lg);
|
||||
}, [lg]);
|
||||
|
||||
return (
|
||||
<DraggablePanel
|
||||
@@ -47,7 +43,7 @@ const Desktop = memo(() => {
|
||||
}}
|
||||
expand={showAgentSettings}
|
||||
minWidth={CHAT_SIDEBAR_WIDTH}
|
||||
mode={'fixed'}
|
||||
mode={md ? 'fixed' : 'float'}
|
||||
onExpandChange={toggleConfig}
|
||||
placement={'right'}
|
||||
showHandlerWideArea={false}
|
||||
@@ -61,8 +57,7 @@ const Desktop = memo(() => {
|
||||
}}
|
||||
>
|
||||
<SafeSpacing />
|
||||
{showSystemRole && !isInbox && <SystemRole />}
|
||||
<TopicListContent />
|
||||
{children}
|
||||
</DraggablePanelContainer>
|
||||
</DraggablePanel>
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { LayoutProps } from '../type';
|
||||
import ChatHeader from './ChatHeader';
|
||||
import HotKeys from './HotKeys';
|
||||
import SideBar from './SideBar';
|
||||
|
||||
const Layout = ({ children, topic, conversation }: LayoutProps) => {
|
||||
return (
|
||||
<>
|
||||
<ChatHeader />
|
||||
<Flexbox
|
||||
height={'100%'}
|
||||
horizontal
|
||||
style={{ overflow: 'hidden', position: 'relative' }}
|
||||
width={'100%'}
|
||||
>
|
||||
{conversation}
|
||||
{children}
|
||||
<SideBar>{topic}</SideBar>
|
||||
</Flexbox>
|
||||
<HotKeys />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Layout.displayName = 'DesktopConversationLayout';
|
||||
|
||||
export default Layout;
|
||||
+5
-3
@@ -1,7 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { MobileNavBar } from '@lobehub/ui';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { memo, useState } from 'react';
|
||||
|
||||
import { useQueryRoute } from '@/hooks/useQueryRoute';
|
||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
|
||||
import SettingButton from '../../../features/SettingButton';
|
||||
@@ -9,7 +11,7 @@ import ShareButton from '../../../features/ShareButton';
|
||||
import ChatHeaderTitle from './ChatHeaderTitle';
|
||||
|
||||
const MobileHeader = memo(() => {
|
||||
const router = useRouter();
|
||||
const router = useQueryRoute();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { isAgentEditable } = useServerConfigStore(featureFlagsSelectors);
|
||||
@@ -32,7 +34,7 @@ const MobileHeader = memo(() => {
|
||||
return (
|
||||
<MobileNavBar
|
||||
center={<ChatHeaderTitle />}
|
||||
onBackClick={() => router.push('/chat')}
|
||||
onBackClick={() => router.push('/chat', { query: { active: '' } })}
|
||||
right={
|
||||
<>
|
||||
<ShareButton mobile open={open} setOpen={setOpen} />
|
||||
+5
-5
@@ -1,12 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { Modal } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
import { PropsWithChildren, memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
|
||||
import TopicListContent from '../../features/TopicListContent';
|
||||
|
||||
const Topics = memo(() => {
|
||||
const Topics = memo(({ children }: PropsWithChildren) => {
|
||||
const [showAgentSettings, toggleConfig] = useGlobalStore((s) => [
|
||||
s.preference.mobileShowTopic,
|
||||
s.toggleMobileTopic,
|
||||
@@ -21,7 +21,7 @@ const Topics = memo(() => {
|
||||
open={showAgentSettings}
|
||||
title={t('topic.title')}
|
||||
>
|
||||
<TopicListContent mobile />
|
||||
{children}
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import MobileContentLayout from '@/components/server/MobileNavLayout';
|
||||
|
||||
import { LayoutProps } from '../type';
|
||||
import ChatHeader from './ChatHeader';
|
||||
import TopicModal from './TopicModal';
|
||||
|
||||
const Layout = ({ children, topic, conversation }: LayoutProps) => {
|
||||
return (
|
||||
<MobileContentLayout header={<ChatHeader />}>
|
||||
{conversation}
|
||||
{children}
|
||||
<TopicModal>{topic}</TopicModal>
|
||||
</MobileContentLayout>
|
||||
);
|
||||
};
|
||||
|
||||
Layout.displayName = 'MobileConversationLayout';
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export interface LayoutProps {
|
||||
children: ReactNode;
|
||||
conversation: ReactNode;
|
||||
topic: ReactNode;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { ActionIcon } from '@lobehub/ui';
|
||||
import { Badge, Button, Tag } from 'antd';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { LucideRotateCw, LucideTrash2, RotateCwIcon } from 'lucide-react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import ManifestPreviewer from '@/components/ManifestPreviewer';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { useToolStore } from '@/store/tool';
|
||||
import { customPluginSelectors, toolSelectors } from '@/store/tool/selectors';
|
||||
|
||||
interface PluginStatusProps {
|
||||
deprecated?: boolean;
|
||||
id: string;
|
||||
title?: string;
|
||||
}
|
||||
const PluginStatus = memo<PluginStatusProps>(({ title, id, deprecated }) => {
|
||||
const { t } = useTranslation('common');
|
||||
const [status, isCustom, reinstallCustomPlugin] = useToolStore((s) => [
|
||||
toolSelectors.getManifestLoadingStatus(id)(s),
|
||||
customPluginSelectors.isCustomPlugin(id)(s),
|
||||
s.reinstallCustomPlugin,
|
||||
]);
|
||||
|
||||
const manifest = useToolStore(toolSelectors.getManifestById(id), isEqual);
|
||||
|
||||
const removePlugin = useAgentStore((s) => s.removePlugin);
|
||||
|
||||
const renderStatus = useMemo(() => {
|
||||
switch (status) {
|
||||
case 'loading': {
|
||||
return <Badge color={'blue'} status={'processing'} />;
|
||||
}
|
||||
case 'error': {
|
||||
return (
|
||||
<ActionIcon
|
||||
icon={LucideRotateCw}
|
||||
onClick={() => {
|
||||
reinstallCustomPlugin(id);
|
||||
}}
|
||||
size={'small'}
|
||||
title={t('retry')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
case 'success': {
|
||||
return <Badge status={'success'} />;
|
||||
}
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
const tag =
|
||||
// 废弃标签
|
||||
deprecated ? (
|
||||
<Tag bordered={false} color={'red'} style={{ marginRight: 0 }}>
|
||||
{t('list.item.deprecated.title', { ns: 'plugin' })}
|
||||
</Tag>
|
||||
) : // 自定义标签
|
||||
isCustom ? (
|
||||
<Tag bordered={false} color={'gold'}>
|
||||
{t('list.item.local.title', { ns: 'plugin' })}
|
||||
</Tag>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Flexbox gap={12} horizontal justify={'space-between'}>
|
||||
<Flexbox align={'center'} gap={8} horizontal>
|
||||
{title || id}
|
||||
{tag}
|
||||
</Flexbox>
|
||||
|
||||
{deprecated ? (
|
||||
<ActionIcon
|
||||
icon={LucideTrash2}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removePlugin(id);
|
||||
}}
|
||||
size={'small'}
|
||||
title={t('plugin.clearDeprecated', { ns: 'setting' })}
|
||||
/>
|
||||
) : (
|
||||
<Flexbox align={'center'} horizontal>
|
||||
{isCustom ? (
|
||||
<ActionIcon
|
||||
icon={RotateCwIcon}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
reinstallCustomPlugin(id);
|
||||
}}
|
||||
size={'small'}
|
||||
title={t('dev.meta.manifest.refresh', { ns: 'plugin' })}
|
||||
/>
|
||||
) : null}
|
||||
<ManifestPreviewer manifest={manifest || {}} trigger={'hover'}>
|
||||
<Button icon={renderStatus} size={'small'} type={'text'} />
|
||||
</ManifestPreviewer>
|
||||
</Flexbox>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default PluginStatus;
|
||||
@@ -0,0 +1,60 @@
|
||||
'use client';
|
||||
|
||||
import { Avatar, Icon, Tag } from '@lobehub/ui';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { Dropdown } from 'antd';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { LucideToyBrick } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
import { pluginHelpers, useToolStore } from '@/store/tool';
|
||||
import { toolSelectors } from '@/store/tool/selectors';
|
||||
|
||||
import PluginStatus from './PluginStatus';
|
||||
|
||||
export interface PluginTagProps {
|
||||
plugins: string[];
|
||||
}
|
||||
|
||||
const PluginTag = memo<PluginTagProps>(({ plugins }) => {
|
||||
const { showDalle } = useServerConfigStore(featureFlagsSelectors);
|
||||
const list = useToolStore(toolSelectors.metaList(showDalle), isEqual);
|
||||
const displayPlugin = useToolStore(toolSelectors.getMetaById(plugins[0]), isEqual);
|
||||
|
||||
if (plugins.length === 0) return null;
|
||||
|
||||
const items: MenuProps['items'] = plugins.map((id) => {
|
||||
const item = list.find((i) => i.identifier === id);
|
||||
const isDeprecated = !pluginHelpers.getPluginTitle(item?.meta);
|
||||
const avatar = isDeprecated ? '♻️' : pluginHelpers.getPluginAvatar(item?.meta);
|
||||
|
||||
return {
|
||||
icon: <Avatar avatar={avatar} size={24} style={{ marginLeft: -6, marginRight: 2 }} />,
|
||||
key: id,
|
||||
label: (
|
||||
<PluginStatus
|
||||
deprecated={isDeprecated}
|
||||
id={id}
|
||||
title={pluginHelpers.getPluginTitle(item?.meta)}
|
||||
/>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const count = plugins.length;
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items }}>
|
||||
<div>
|
||||
<Tag>
|
||||
{<Icon icon={LucideToyBrick} />}
|
||||
{pluginHelpers.getPluginTitle(displayPlugin) || plugins[0]}
|
||||
{count > 1 && <div>({plugins.length - 1}+)</div>}
|
||||
</Tag>
|
||||
</div>
|
||||
</Dropdown>
|
||||
);
|
||||
});
|
||||
|
||||
export default PluginTag;
|
||||
@@ -0,0 +1,40 @@
|
||||
'use client';
|
||||
|
||||
import { ActionIcon } from '@lobehub/ui';
|
||||
import { AlignJustify } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { DESKTOP_HEADER_ICON_SIZE, MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { SidebarTabKey } from '@/store/global/initialState';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { sessionSelectors } from '@/store/session/selectors';
|
||||
import { pathString } from '@/utils/url';
|
||||
|
||||
const SettingButton = memo<{ mobile?: boolean }>(({ mobile }) => {
|
||||
const isInbox = useSessionStore(sessionSelectors.isInboxSession);
|
||||
const { t } = useTranslation('common');
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<ActionIcon
|
||||
icon={AlignJustify}
|
||||
onClick={() => {
|
||||
if (isInbox) {
|
||||
useGlobalStore.setState({
|
||||
sidebarKey: SidebarTabKey.Setting,
|
||||
});
|
||||
router.push('/settings/agent');
|
||||
} else {
|
||||
router.push(pathString('/chat/settings', { search: location.search }));
|
||||
}
|
||||
}}
|
||||
size={mobile ? MOBILE_HEADER_ICON_SIZE : DESKTOP_HEADER_ICON_SIZE}
|
||||
title={t('header.session', { ns: 'setting' })}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default SettingButton;
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Avatar, ChatHeaderTitle, Logo, Markdown } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import pkg from '@/../package.json';
|
||||
import ModelTag from '@/components/ModelTag';
|
||||
import ChatList from '@/features/Conversation/components/ChatList';
|
||||
import { useAgentStore } from '@/store/agent';
|
||||
import { agentSelectors } from '@/store/agent/selectors';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { sessionMetaSelectors, sessionSelectors } from '@/store/session/selectors';
|
||||
|
||||
import PluginTag from '../PluginTag';
|
||||
import { useStyles } from './style';
|
||||
import { FieldType } from './type';
|
||||
|
||||
const Preview = memo<FieldType & { title?: string }>(
|
||||
({ title, withSystemRole, withBackground, withFooter }) => {
|
||||
const [model, plugins, systemRole] = useAgentStore((s) => [
|
||||
agentSelectors.currentAgentModel(s),
|
||||
agentSelectors.currentAgentPlugins(s),
|
||||
agentSelectors.currentAgentSystemRole(s),
|
||||
]);
|
||||
const [isInbox, description, avatar, backgroundColor] = useSessionStore((s) => [
|
||||
sessionSelectors.isInboxSession(s),
|
||||
sessionMetaSelectors.currentAgentDescription(s),
|
||||
sessionMetaSelectors.currentAgentAvatar(s),
|
||||
sessionMetaSelectors.currentAgentBackgroundColor(s),
|
||||
]);
|
||||
|
||||
const { t } = useTranslation('chat');
|
||||
const { styles } = useStyles(withBackground);
|
||||
|
||||
const displayTitle = isInbox ? t('inbox.title') : title;
|
||||
const displayDesc = isInbox ? t('inbox.desc') : description;
|
||||
|
||||
return (
|
||||
<div className={styles.preview}>
|
||||
<div className={withBackground ? styles.background : undefined} id={'preview'}>
|
||||
<Flexbox className={styles.container} gap={16}>
|
||||
<div className={styles.header}>
|
||||
<Flexbox align={'flex-start'} gap={12} horizontal>
|
||||
<Avatar avatar={avatar} background={backgroundColor} size={40} title={title} />
|
||||
<ChatHeaderTitle
|
||||
desc={displayDesc}
|
||||
tag={
|
||||
<>
|
||||
<ModelTag model={model} />
|
||||
{plugins?.length > 0 && <PluginTag plugins={plugins} />}
|
||||
</>
|
||||
}
|
||||
title={displayTitle}
|
||||
/>
|
||||
</Flexbox>
|
||||
{withSystemRole && systemRole && (
|
||||
<div className={styles.role}>
|
||||
<Markdown variant={'chat'}>{systemRole}</Markdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ChatList />
|
||||
{withFooter ? (
|
||||
<Flexbox align={'center'} className={styles.footer} gap={4}>
|
||||
<Logo extra={'chat'} type={'combine'} />
|
||||
<div className={styles.url}>{pkg.homepage}</div>
|
||||
</Flexbox>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</Flexbox>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default Preview;
|
||||
@@ -0,0 +1,164 @@
|
||||
import { Form, type FormItemProps, Modal, type ModalProps } from '@lobehub/ui';
|
||||
import { Button, Segmented, SegmentedProps, Switch } from 'antd';
|
||||
import { memo, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { FORM_STYLE } from '@/const/layoutTokens';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { userProfileSelectors } from '@/store/user/selectors';
|
||||
|
||||
import Preview from './Preview';
|
||||
import { FieldType, ImageType } from './type';
|
||||
import { useScreenshot } from './useScreenshot';
|
||||
|
||||
enum Tab {
|
||||
Screenshot = 'screenshot',
|
||||
ShareGPT = 'sharegpt',
|
||||
}
|
||||
|
||||
export const imageTypeOptions: SegmentedProps['options'] = [
|
||||
{
|
||||
label: 'JPG',
|
||||
value: ImageType.JPG,
|
||||
},
|
||||
{
|
||||
label: 'PNG',
|
||||
value: ImageType.PNG,
|
||||
},
|
||||
{
|
||||
label: 'SVG',
|
||||
value: ImageType.SVG,
|
||||
},
|
||||
{
|
||||
label: 'WEBP',
|
||||
value: ImageType.WEBP,
|
||||
},
|
||||
];
|
||||
|
||||
const DEFAULT_FIELD_VALUE: FieldType = {
|
||||
imageType: ImageType.JPG,
|
||||
withBackground: true,
|
||||
withFooter: false,
|
||||
withPluginInfo: false,
|
||||
withSystemRole: false,
|
||||
};
|
||||
|
||||
const ShareModal = memo<ModalProps>(({ onCancel, open }) => {
|
||||
const [fieldValue, setFieldValue] = useState<FieldType>(DEFAULT_FIELD_VALUE);
|
||||
const [tab, setTab] = useState<Tab>(Tab.Screenshot);
|
||||
const { t } = useTranslation('chat');
|
||||
const avatar = useUserStore(userProfileSelectors.userAvatar);
|
||||
const [shareLoading, shareToShareGPT] = useChatStore((s) => [s.shareLoading, s.shareToShareGPT]);
|
||||
const { loading, onDownload, title } = useScreenshot(fieldValue.imageType);
|
||||
|
||||
const options: SegmentedProps['options'] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t('shareModal.screenshot'),
|
||||
value: Tab.Screenshot,
|
||||
},
|
||||
{
|
||||
label: 'ShareGPT',
|
||||
value: Tab.ShareGPT,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const settings: FormItemProps[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
children: <Switch />,
|
||||
label: t('shareModal.withSystemRole'),
|
||||
minWidth: undefined,
|
||||
name: 'withSystemRole',
|
||||
valuePropName: 'checked',
|
||||
},
|
||||
{
|
||||
children: <Switch />,
|
||||
hidden: tab !== Tab.Screenshot,
|
||||
label: t('shareModal.withBackground'),
|
||||
minWidth: undefined,
|
||||
name: 'withBackground',
|
||||
valuePropName: 'checked',
|
||||
},
|
||||
{
|
||||
children: <Switch />,
|
||||
hidden: tab !== Tab.Screenshot,
|
||||
label: t('shareModal.withFooter'),
|
||||
minWidth: undefined,
|
||||
name: 'withFooter',
|
||||
valuePropName: 'checked',
|
||||
},
|
||||
{
|
||||
children: <Segmented options={imageTypeOptions} />,
|
||||
hidden: tab !== Tab.Screenshot,
|
||||
label: t('shareModal.imageType'),
|
||||
minWidth: undefined,
|
||||
name: 'imageType',
|
||||
},
|
||||
{
|
||||
children: <Switch />,
|
||||
hidden: tab !== Tab.ShareGPT,
|
||||
label: t('shareModal.withPluginInfo'),
|
||||
minWidth: undefined,
|
||||
name: 'withPluginInfo',
|
||||
valuePropName: 'checked',
|
||||
},
|
||||
],
|
||||
[tab],
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
allowFullscreen
|
||||
centered={false}
|
||||
footer={
|
||||
<>
|
||||
{tab === Tab.Screenshot && (
|
||||
<Button block loading={loading} onClick={onDownload} size={'large'} type={'primary'}>
|
||||
{t('shareModal.download')}
|
||||
</Button>
|
||||
)}
|
||||
{tab === Tab.ShareGPT && (
|
||||
<Button
|
||||
block
|
||||
loading={shareLoading}
|
||||
onClick={() => shareToShareGPT({ avatar, ...fieldValue })}
|
||||
size={'large'}
|
||||
type={'primary'}
|
||||
>
|
||||
{t('shareModal.shareToShareGPT')}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
maxHeight={false}
|
||||
onCancel={onCancel}
|
||||
open={open}
|
||||
title={t('share', { ns: 'common' })}
|
||||
>
|
||||
<Flexbox gap={16}>
|
||||
<Segmented
|
||||
block
|
||||
onChange={(value) => setTab(value as Tab)}
|
||||
options={options}
|
||||
style={{ width: '100%' }}
|
||||
value={tab}
|
||||
/>
|
||||
{tab === Tab.Screenshot && <Preview title={title} {...fieldValue} />}
|
||||
<Form
|
||||
initialValues={DEFAULT_FIELD_VALUE}
|
||||
items={settings}
|
||||
itemsType={'flat'}
|
||||
onValuesChange={(_, v) => setFieldValue(v)}
|
||||
{...FORM_STYLE}
|
||||
/>
|
||||
</Flexbox>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export default ShareModal;
|
||||
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { ActionIcon } from '@lobehub/ui';
|
||||
import { Share2 } from 'lucide-react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useMergeState from 'use-merge-value';
|
||||
|
||||
import { DESKTOP_HEADER_ICON_SIZE, MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
|
||||
const ShareModal = dynamic(() => import('./ShareModal'));
|
||||
interface ShareButtonProps {
|
||||
mobile?: boolean;
|
||||
open?: boolean;
|
||||
setOpen?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const ShareButton = memo<ShareButtonProps>(({ mobile, setOpen, open }) => {
|
||||
const [isModalOpen, setIsModalOpen] = useMergeState(false, {
|
||||
defaultValue: false,
|
||||
onChange: setOpen,
|
||||
value: open,
|
||||
});
|
||||
const { t } = useTranslation('common');
|
||||
const [shareLoading] = useChatStore((s) => [s.shareLoading]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionIcon
|
||||
icon={Share2}
|
||||
loading={shareLoading}
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
size={mobile ? MOBILE_HEADER_ICON_SIZE : DESKTOP_HEADER_ICON_SIZE}
|
||||
title={t('share')}
|
||||
/>
|
||||
<ShareModal onCancel={() => setIsModalOpen(false)} open={isModalOpen} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default ShareButton;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
import { imageUrl } from '@/const/url';
|
||||
|
||||
export const useStyles = createStyles(({ css, token, stylish, cx }, withBackground: boolean) => ({
|
||||
background: css`
|
||||
padding: 24px;
|
||||
|
||||
background-color: ${token.colorBgLayout};
|
||||
background-image: url(${imageUrl('screenshot_background.webp')});
|
||||
background-position: center;
|
||||
background-size: 120% 120%;
|
||||
`,
|
||||
container: cx(
|
||||
withBackground &&
|
||||
css`
|
||||
overflow: hidden;
|
||||
border: 2px solid ${token.colorBorder};
|
||||
border-radius: ${token.borderRadiusLG}px;
|
||||
`,
|
||||
|
||||
css`
|
||||
background: ${token.colorBgLayout};
|
||||
`,
|
||||
),
|
||||
footer: css`
|
||||
padding: 16px;
|
||||
border-top: 1px solid ${token.colorBorder};
|
||||
`,
|
||||
header: css`
|
||||
margin-bottom: -24px;
|
||||
padding: 16px;
|
||||
background: ${token.colorBgContainer};
|
||||
border-bottom: 1px solid ${token.colorBorder};
|
||||
`,
|
||||
preview: cx(
|
||||
stylish.noScrollbar,
|
||||
css`
|
||||
overflow: hidden scroll;
|
||||
|
||||
width: 100%;
|
||||
max-height: 40dvh;
|
||||
|
||||
background: ${token.colorBgLayout};
|
||||
border: 1px solid ${token.colorBorder};
|
||||
border-radius: ${token.borderRadiusLG}px;
|
||||
|
||||
* {
|
||||
pointer-events: none;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
}
|
||||
`,
|
||||
),
|
||||
role: css`
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
opacity: 0.75;
|
||||
border-top: 1px dashed ${token.colorBorderSecondary};
|
||||
|
||||
* {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
`,
|
||||
url: css`
|
||||
color: ${token.colorTextDescription};
|
||||
`,
|
||||
}));
|
||||
@@ -0,0 +1,14 @@
|
||||
export enum ImageType {
|
||||
JPG = 'jpg',
|
||||
PNG = 'png',
|
||||
SVG = 'svg',
|
||||
WEBP = 'webp',
|
||||
}
|
||||
|
||||
export type FieldType = {
|
||||
imageType: ImageType;
|
||||
withBackground: boolean;
|
||||
withFooter: boolean;
|
||||
withPluginInfo: boolean;
|
||||
withSystemRole: boolean;
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import dayjs from 'dayjs';
|
||||
import { domToJpeg, domToPng, domToSvg, domToWebp } from 'modern-screenshot';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { sessionMetaSelectors } from '@/store/session/selectors';
|
||||
|
||||
import { ImageType } from './type';
|
||||
|
||||
export const useScreenshot = (imageType: ImageType) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const title = useSessionStore(sessionMetaSelectors.currentAgentTitle);
|
||||
|
||||
const handleDownload = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
let screenshotFn: any;
|
||||
switch (imageType) {
|
||||
case ImageType.JPG: {
|
||||
screenshotFn = domToJpeg;
|
||||
break;
|
||||
}
|
||||
case ImageType.PNG: {
|
||||
screenshotFn = domToPng;
|
||||
break;
|
||||
}
|
||||
case ImageType.SVG: {
|
||||
screenshotFn = domToSvg;
|
||||
break;
|
||||
}
|
||||
case ImageType.WEBP: {
|
||||
screenshotFn = domToWebp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const dataUrl = await screenshotFn(document.querySelector('#preview') as HTMLDivElement, {
|
||||
features: {
|
||||
// 不启用移除控制符,否则会导致 safari emoji 报错
|
||||
removeControlCharacter: false,
|
||||
},
|
||||
scale: 2,
|
||||
});
|
||||
const link = document.createElement('a');
|
||||
link.download = `LobeChat_${title}_${dayjs().format('YYYY-MM-DD')}.${imageType}`;
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
setLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Failed to download image', error);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [imageType, title]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
onDownload: handleDownload,
|
||||
title,
|
||||
};
|
||||
};
|
||||
+2
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { Avatar, Icon } from '@lobehub/ui';
|
||||
import { Button } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
@@ -0,0 +1,11 @@
|
||||
import ServerLayout from '@/components/server/ServerLayout';
|
||||
|
||||
import Desktop from './_layout/Desktop';
|
||||
import Mobile from './_layout/Mobile';
|
||||
import { LayoutProps } from './_layout/type';
|
||||
|
||||
const Layout = ServerLayout<LayoutProps>({ Desktop, Mobile });
|
||||
|
||||
Layout.displayName = 'ChatConversationLayout';
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { isMobileDevice } from '@/utils/responsive';
|
||||
|
||||
import PageTitle from '../features/PageTitle';
|
||||
import TelemetryNotification from './features/TelemetryNotification';
|
||||
|
||||
const Page = () => {
|
||||
const mobile = isMobileDevice();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle />
|
||||
<TelemetryNotification mobile={mobile} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Page.displayName = 'Chat';
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { DraggablePanelBody } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { PropsWithChildren, memo } from 'react';
|
||||
|
||||
const useStyles = createStyles(
|
||||
({ css }) => css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 8px 8px 0;
|
||||
`,
|
||||
);
|
||||
|
||||
const PanelBody = memo<PropsWithChildren>(({ children }) => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return <DraggablePanelBody className={styles}>{children}</DraggablePanelBody>;
|
||||
});
|
||||
|
||||
export default PanelBody;
|
||||
+2
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { ActionIcon, Logo } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { MessageSquarePlus } from 'lucide-react';
|
||||
@@ -0,0 +1,15 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import PanelBody from './PanelBody';
|
||||
import Header from './SessionHeader';
|
||||
|
||||
const DesktopLayout = ({ children }: PropsWithChildren) => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<PanelBody>{children}</PanelBody>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DesktopLayout;
|
||||
@@ -0,0 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import { ActionIcon, Logo, MobileNavBar } from '@lobehub/ui';
|
||||
import { MessageSquarePlus } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { MOBILE_HEADER_ICON_SIZE } from '@/const/layoutTokens';
|
||||
import SyncStatusInspector from '@/features/SyncStatusInspector';
|
||||
import UserAvatar from '@/features/User/UserAvatar';
|
||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { mobileHeaderSticky } from '@/styles/mobileHeader';
|
||||
|
||||
const Header = memo(() => {
|
||||
const [createSession] = useSessionStore((s) => [s.createSession]);
|
||||
const router = useRouter();
|
||||
const { showCreateSession, enableWebrtc } = useServerConfigStore(featureFlagsSelectors);
|
||||
|
||||
return (
|
||||
<MobileNavBar
|
||||
center={<Logo type={'text'} />}
|
||||
left={<UserAvatar onClick={() => router.push('/me')} size={32} />}
|
||||
right={
|
||||
<>
|
||||
{enableWebrtc && <SyncStatusInspector mobile />}
|
||||
{showCreateSession && (
|
||||
<ActionIcon
|
||||
icon={MessageSquarePlus}
|
||||
onClick={() => createSession()}
|
||||
size={MOBILE_HEADER_ICON_SIZE}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
style={mobileHeaderSticky}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import MobileContentLayout from '@/components/server/MobileNavLayout';
|
||||
|
||||
import SessionSearchBar from '../../features/SessionSearchBar';
|
||||
import SessionHeader from './SessionHeader';
|
||||
|
||||
const MobileLayout = ({ children }: PropsWithChildren) => {
|
||||
return (
|
||||
<MobileContentLayout header={<SessionHeader />} withNav>
|
||||
<div style={{ padding: '8px 16px' }}>
|
||||
<SessionSearchBar mobile />
|
||||
</div>
|
||||
{children}
|
||||
</MobileContentLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default MobileLayout;
|
||||
@@ -0,0 +1,26 @@
|
||||
import Migration from '@/app/(main)/chat/features/Migration';
|
||||
import ServerLayout from '@/components/server/ServerLayout';
|
||||
|
||||
import Desktop from './_layout/Desktop';
|
||||
import Mobile from './_layout/Mobile';
|
||||
import SessionHydration from './features/SessionHydration';
|
||||
import SessionListContent from './features/SessionListContent';
|
||||
|
||||
const Layout = ServerLayout({ Desktop, Mobile });
|
||||
|
||||
const Session = () => {
|
||||
return (
|
||||
<>
|
||||
<Migration>
|
||||
<Layout>
|
||||
<SessionListContent />
|
||||
</Layout>
|
||||
</Migration>
|
||||
<SessionHydration />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Session.displayName = 'Session';
|
||||
|
||||
export default Session;
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client';
|
||||
|
||||
import { useResponsive } from 'antd-style';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { parseAsString } from 'nuqs/server';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { createStoreUpdater } from 'zustand-utils';
|
||||
|
||||
import { useSessionStore } from '@/store/session';
|
||||
|
||||
// sync outside state to useSessionStore
|
||||
const SessionHydration = memo(() => {
|
||||
const useStoreUpdater = createStoreUpdater(useSessionStore);
|
||||
|
||||
const { mobile } = useResponsive();
|
||||
useStoreUpdater('isMobile', mobile);
|
||||
|
||||
// two-way bindings the url and session store
|
||||
const [session, setSession] = useQueryState(
|
||||
'session',
|
||||
parseAsString.withDefault('inbox').withOptions({ history: 'replace', throttleMs: 500 }),
|
||||
);
|
||||
useStoreUpdater('activeId', session);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = useSessionStore.subscribe(
|
||||
(s) => s.activeId,
|
||||
(state) => setSession(state),
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
export default SessionHydration;
|
||||
+2
-4
@@ -16,7 +16,7 @@ import SessionList from './List';
|
||||
import ConfigGroupModal from './Modals/ConfigGroupModal';
|
||||
import RenameGroupModal from './Modals/RenameGroupModal';
|
||||
|
||||
const SessionDefaultMode = memo(() => {
|
||||
const SessionListContent = memo(() => {
|
||||
const { t } = useTranslation('chat');
|
||||
|
||||
const [activeGroupId, setActiveGroupId] = useState<string>();
|
||||
@@ -98,6 +98,4 @@ const SessionDefaultMode = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
SessionDefaultMode.displayName = 'SessionDefaultMode';
|
||||
|
||||
export default SessionDefaultMode;
|
||||
export default SessionListContent;
|
||||
+1
-11
@@ -1,13 +1,9 @@
|
||||
import { Empty } from 'antd';
|
||||
import { createStyles, useResponsive } from 'antd-style';
|
||||
import Link from 'next/link';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Center } from 'react-layout-kit';
|
||||
import LazyLoad from 'react-lazy-load';
|
||||
|
||||
import { SESSION_CHAT_URL } from '@/const/url';
|
||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { sessionSelectors } from '@/store/session/selectors';
|
||||
import { LobeAgentSession } from '@/types/session';
|
||||
@@ -28,9 +24,7 @@ interface SessionListProps {
|
||||
showAddButton?: boolean;
|
||||
}
|
||||
const SessionList = memo<SessionListProps>(({ dataSource, groupId, showAddButton = true }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const isInit = useSessionStore((s) => sessionSelectors.isSessionListInit(s));
|
||||
const { showCreateSession } = useServerConfigStore(featureFlagsSelectors);
|
||||
const { styles } = useStyles();
|
||||
|
||||
const { mobile } = useResponsive();
|
||||
@@ -46,12 +40,8 @@ const SessionList = memo<SessionListProps>(({ dataSource, groupId, showAddButton
|
||||
</Link>
|
||||
</LazyLoad>
|
||||
))
|
||||
) : showCreateSession ? (
|
||||
showAddButton && <AddButton groupId={groupId} />
|
||||
) : (
|
||||
<Center>
|
||||
<Empty description={t('emptyAgent')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
</Center>
|
||||
showAddButton && <AddButton groupId={groupId} />
|
||||
);
|
||||
});
|
||||
|
||||
+2
-2
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useSessionStore } from '@/store/session';
|
||||
@@ -11,6 +13,4 @@ const SessionListContent = memo(() => {
|
||||
return isSearching ? <SearchMode /> : <DefaultMode />;
|
||||
});
|
||||
|
||||
SessionListContent.displayName = 'SessionListContent';
|
||||
|
||||
export default SessionListContent;
|
||||
+3
-5
@@ -1,11 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { SearchBar } from '@lobehub/ui';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
|
||||
const SessionSearchBar = memo<{ mobile?: boolean }>(({ mobile: controlledMobile }) => {
|
||||
const SessionSearchBar = memo<{ mobile?: boolean }>(({ mobile }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
|
||||
const [keywords, useSearchSessions, updateSearchKeywords] = useSessionStore((s) => [
|
||||
@@ -16,9 +17,6 @@ const SessionSearchBar = memo<{ mobile?: boolean }>(({ mobile: controlledMobile
|
||||
|
||||
const { isValidating } = useSearchSessions(keywords);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const mobile = controlledMobile ?? isMobile;
|
||||
|
||||
return (
|
||||
<SearchBar
|
||||
allowClear
|
||||
@@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import { ReactNode } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import FolderPanel from '@/features/FolderPanel';
|
||||
|
||||
type Props = { children: ReactNode; session: ReactNode };
|
||||
|
||||
const Layout = ({ children, session }: Props) => {
|
||||
return (
|
||||
<Flexbox
|
||||
height={'100%'}
|
||||
horizontal
|
||||
style={{ maxWidth: 'calc(100vw - 64px)', overflow: 'hidden', position: 'relative' }}
|
||||
width={'100%'}
|
||||
>
|
||||
<FolderPanel>{session}</FolderPanel>
|
||||
<Flexbox
|
||||
flex={1}
|
||||
id={'lobe-conversion-container'}
|
||||
style={{ overflow: 'hidden', position: 'relative' }}
|
||||
>
|
||||
{children}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
);
|
||||
};
|
||||
|
||||
Layout.displayName = 'DesktopChatLayout';
|
||||
|
||||
export default Layout;
|
||||
@@ -1,39 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { DraggablePanelBody } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
|
||||
import FolderPanel from '@/features/FolderPanel';
|
||||
|
||||
import SessionListContent from '../../features/SessionListContent';
|
||||
import Header from './SessionHeader';
|
||||
|
||||
const useStyles = createStyles(({ stylish, css, cx }) =>
|
||||
cx(
|
||||
stylish.noScrollbar,
|
||||
css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: 8px 8px 0;
|
||||
`,
|
||||
),
|
||||
);
|
||||
|
||||
const Sessions = memo(() => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
return (
|
||||
<FolderPanel>
|
||||
<Header />
|
||||
<DraggablePanelBody className={styles}>
|
||||
<SessionListContent />
|
||||
</DraggablePanelBody>
|
||||
</FolderPanel>
|
||||
);
|
||||
});
|
||||
|
||||
Sessions.displayName = 'SessionsList';
|
||||
|
||||
export default Sessions;
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { LayoutProps } from '../type';
|
||||
import ResponsiveSessionList from './SessionList';
|
||||
|
||||
const Layout = ({ children }: LayoutProps) => {
|
||||
return (
|
||||
<>
|
||||
<ResponsiveSessionList />
|
||||
<Flexbox
|
||||
flex={1}
|
||||
height={'100%'}
|
||||
id={'lobe-conversion-container'}
|
||||
style={{ position: 'relative' }}
|
||||
>
|
||||
{children}
|
||||
</Flexbox>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Layout.displayName = 'DesktopChatLayout';
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client';
|
||||
|
||||
import { createStyles } from 'antd-style';
|
||||
import { ReactNode, memo } from 'react';
|
||||
|
||||
import { useQuery } from '@/hooks/useQuery';
|
||||
|
||||
type Props = { children: ReactNode; session: ReactNode };
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
active: css`
|
||||
transform: translateX(0);
|
||||
`,
|
||||
mask: css`
|
||||
position: absolute;
|
||||
z-index: 101;
|
||||
inset: 0;
|
||||
|
||||
background: ${token.colorBgMask};
|
||||
backdrop-filter: blur(2px);
|
||||
`,
|
||||
subPage: css`
|
||||
position: absolute;
|
||||
z-index: 102;
|
||||
inset: 0;
|
||||
transform: translateX(100%);
|
||||
|
||||
background: ${token.colorBgLayout};
|
||||
box-shadow: ${token.boxShadow};
|
||||
|
||||
transition: transform 0.3s ${token.motionEaseInOut};
|
||||
`,
|
||||
}));
|
||||
|
||||
const Layout = memo<Props>(({ children, session }) => {
|
||||
const { active } = useQuery();
|
||||
const { cx, styles } = useStyles();
|
||||
|
||||
return (
|
||||
<>
|
||||
{session}
|
||||
{active && <div className={styles.mask} />}
|
||||
<div className={cx(styles.subPage, active && styles.active)}>{children}</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
Layout.displayName = 'MobileChatLayout';
|
||||
|
||||
export default Layout;
|
||||
@@ -1,9 +0,0 @@
|
||||
import { LayoutProps } from '../type';
|
||||
|
||||
const Layout = ({ children }: LayoutProps) => {
|
||||
return children;
|
||||
};
|
||||
|
||||
Layout.displayName = 'MobileChatLayout';
|
||||
|
||||
export default Layout;
|
||||
@@ -2,4 +2,5 @@ import { ReactNode } from 'react';
|
||||
|
||||
export interface LayoutProps {
|
||||
children: ReactNode;
|
||||
session: ReactNode;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
export default dynamic(() => import('@/components/Error'));
|
||||
@@ -13,7 +13,7 @@ const SettingButton = memo<{ mobile?: boolean }>(({ mobile }) => {
|
||||
return (
|
||||
<ActionIcon
|
||||
icon={AlignJustify}
|
||||
onClick={openChatSettings}
|
||||
onClick={() => openChatSettings()}
|
||||
size={mobile ? MOBILE_HEADER_ICON_SIZE : DESKTOP_HEADER_ICON_SIZE}
|
||||
title={t('header.session', { ns: 'setting' })}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
export default dynamic(() => import('@/components/404'));
|
||||
@@ -1,25 +0,0 @@
|
||||
import { isMobileDevice } from '@/utils/responsive';
|
||||
|
||||
import DesktopPage from './(desktop)';
|
||||
import MobilePage from './(mobile)';
|
||||
import SessionHydration from './components/SessionHydration';
|
||||
import Migration from './features/Migration';
|
||||
import PageTitle from './features/PageTitle';
|
||||
|
||||
const Page = () => {
|
||||
const mobile = isMobileDevice();
|
||||
|
||||
const Page = mobile ? MobilePage : DesktopPage;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Migration>
|
||||
<PageTitle />
|
||||
<Page />
|
||||
</Migration>
|
||||
<SessionHydration />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -6,7 +6,6 @@ import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { mobileHeaderSticky } from '@/styles/mobileHeader';
|
||||
import { pathString } from '@/utils/url';
|
||||
|
||||
import HeaderContent from '../../features/HeaderContent';
|
||||
|
||||
@@ -17,7 +16,7 @@ const Header = memo(() => {
|
||||
return (
|
||||
<MobileNavBar
|
||||
center={<MobileNavBarTitle title={t('header.session')} />}
|
||||
onBackClick={() => router.push(pathString('/chat/mobile', { search: location.search }))}
|
||||
onBackClick={() => router.back()}
|
||||
right={<HeaderContent />}
|
||||
showBackButton
|
||||
style={mobileHeaderSticky}
|
||||
|
||||
@@ -26,6 +26,7 @@ const useStyles = createStyles(
|
||||
|
||||
background: ${inverseTheme ? rgba(token.colorTextTertiary, 0.15) : token.colorFillTertiary};
|
||||
border-radius: ${token.borderRadius}px;
|
||||
box-shadow: 0 0 0 1px ${rgba(token.colorBorder, 0.1)} inset;
|
||||
}
|
||||
`,
|
||||
);
|
||||
@@ -67,7 +68,7 @@ const HotKeys = memo<HotKeysProps>(({ keys, desc, inverseTheme }) => {
|
||||
|
||||
if (!desc) return content;
|
||||
return (
|
||||
<Flexbox gap={16} horizontal>
|
||||
<Flexbox align={'center'} gap={4} style={{ paddingBottom: 4 }}>
|
||||
{desc}
|
||||
{content}
|
||||
</Flexbox>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { memo, useLayoutEffect } from 'react';
|
||||
import { createStoreUpdater } from 'zustand-utils';
|
||||
|
||||
import { useChatStore } from '@/store/chat';
|
||||
@@ -14,7 +14,7 @@ const ChatHydration = memo(() => {
|
||||
const [topic, setTopic] = useQueryState('topic', { history: 'replace', throttleMs: 500 });
|
||||
useStoreUpdater('activeTopicId', topic);
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
const unsubscribe = useChatStore.subscribe(
|
||||
(s) => s.activeTopicId,
|
||||
(state) => {
|
||||
|
||||
@@ -5,6 +5,8 @@ import { merge } from '@/utils/merge';
|
||||
|
||||
export const INBOX_SESSION_ID = 'inbox';
|
||||
|
||||
export const WELCOME_GUIDE_CHAT_ID = 'welcome';
|
||||
|
||||
export const DEFAULT_AGENT_LOBE_SESSION: LobeAgentSession = {
|
||||
config: DEFAULT_AGENT_CONFIG,
|
||||
createdAt: Date.now(),
|
||||
|
||||
+5
-1
@@ -1,3 +1,4 @@
|
||||
import qs from 'query-string';
|
||||
import urlJoin from 'url-join';
|
||||
|
||||
import { withBasePath } from '@/utils/basePath';
|
||||
@@ -40,7 +41,10 @@ export const AGENTS_INDEX_GITHUB = 'https://github.com/lobehub/lobe-chat-agents'
|
||||
export const AGENTS_INDEX_GITHUB_ISSUE = urlJoin(AGENTS_INDEX_GITHUB, 'issues/new');
|
||||
|
||||
export const SESSION_CHAT_URL = (id: string = INBOX_SESSION_ID, mobile?: boolean) =>
|
||||
mobile ? `/chat/mobile?session=${id}` : `/chat?session=${id}`;
|
||||
qs.stringifyUrl({
|
||||
query: mobile ? { active: mobile, session: id } : { session: id },
|
||||
url: '/chat',
|
||||
});
|
||||
|
||||
export const imageUrl = (filename: string) => withBasePath(`/images/${filename}`);
|
||||
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
'use client';
|
||||
|
||||
import { Skeleton } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
const useStyles = createStyles(({ css, prefixCls }) => ({
|
||||
user: css`
|
||||
message: css`
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
.${prefixCls}-skeleton-header {
|
||||
padding: 0;
|
||||
}
|
||||
`,
|
||||
|
||||
right: css`
|
||||
flex-direction: row-reverse;
|
||||
gap: 16px;
|
||||
|
||||
.${prefixCls}-skeleton-paragraph {
|
||||
display: flex;
|
||||
@@ -20,18 +28,24 @@ interface SkeletonListProps {
|
||||
mobile?: boolean;
|
||||
}
|
||||
const SkeletonList = memo<SkeletonListProps>(({ mobile }) => {
|
||||
const { styles } = useStyles();
|
||||
const { cx, styles } = useStyles();
|
||||
|
||||
return (
|
||||
<Flexbox gap={24} padding={12} style={{ marginTop: 24 + (mobile ? 0 : 64) }}>
|
||||
<Flexbox gap={24} padding={mobile ? 8 : 12} style={{ marginTop: 24 + (mobile ? 0 : 64) }}>
|
||||
<Skeleton
|
||||
active
|
||||
avatar={{ size: 40 }}
|
||||
className={styles.user}
|
||||
paragraph={{ width: ['50%', '30%'] }}
|
||||
avatar={{ size: mobile ? 32 : 40 }}
|
||||
className={styles.message}
|
||||
paragraph={{ width: mobile ? ['80%', '40%'] : ['50%', '30%'] }}
|
||||
title={false}
|
||||
/>
|
||||
<Skeleton
|
||||
active
|
||||
avatar={{ size: mobile ? 32 : 40 }}
|
||||
className={cx(styles.message, styles.right)}
|
||||
paragraph={{ width: mobile ? ['80%', '40%'] : ['50%', '30%'] }}
|
||||
title={false}
|
||||
/>
|
||||
<Skeleton active avatar={{ size: 40 }} paragraph={{ width: ['50%', '30%'] }} title={false} />
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
|
||||
import { WELCOME_GUIDE_CHAT_ID } from '@/const/session';
|
||||
|
||||
import Item from '../ChatItem';
|
||||
import InboxWelcome from '../InboxWelcome';
|
||||
|
||||
const ItemContent = memo<{ id: string; index: number; mobile?: boolean }>(
|
||||
({ index, id, mobile }) => {
|
||||
if (id === WELCOME_GUIDE_CHAT_ID) return <InboxWelcome />;
|
||||
|
||||
return index === 0 ? (
|
||||
<div style={{ height: 24 + (mobile ? 0 : 64) }} />
|
||||
) : (
|
||||
<Item id={id} index={index - 1} />
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default ItemContent;
|
||||
@@ -1,31 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
|
||||
import { WELCOME_GUIDE_CHAT_ID } from '@/const/session';
|
||||
import { useChatStore } from '@/store/chat';
|
||||
import { chatSelectors } from '@/store/chat/selectors';
|
||||
import { isMobileScreen } from '@/utils/screen';
|
||||
|
||||
import { useInitConversation } from '../../hooks/useInitConversation';
|
||||
import AutoScroll from '../AutoScroll';
|
||||
import Item from '../ChatItem';
|
||||
import InboxWelcome from '../InboxWelcome';
|
||||
import SkeletonList from '../SkeletonList';
|
||||
|
||||
const WELCOME_ID = 'welcome';
|
||||
|
||||
const itemContent = (index: number, id: string) => {
|
||||
const isMobile = isMobileScreen();
|
||||
|
||||
if (id === WELCOME_ID) return <InboxWelcome />;
|
||||
|
||||
return index === 0 ? (
|
||||
<div style={{ height: 24 + (isMobile ? 0 : 64) }} />
|
||||
) : (
|
||||
<Item id={id} index={index - 1} />
|
||||
);
|
||||
};
|
||||
import ItemContent from './ItemContent';
|
||||
|
||||
interface VirtualizedListProps {
|
||||
mobile?: boolean;
|
||||
@@ -44,7 +31,9 @@ const VirtualizedList = memo<VirtualizedListProps>(({ mobile }) => {
|
||||
|
||||
const data = useChatStore((s) => {
|
||||
const showInboxWelcome = chatSelectors.showInboxWelcome(s);
|
||||
const ids = showInboxWelcome ? [WELCOME_ID] : chatSelectors.currentChatIDsWithGuideMessage(s);
|
||||
const ids = showInboxWelcome
|
||||
? [WELCOME_GUIDE_CHAT_ID]
|
||||
: chatSelectors.currentChatIDsWithGuideMessage(s);
|
||||
return ['empty', ...ids];
|
||||
}, isEqual);
|
||||
|
||||
@@ -78,7 +67,7 @@ const VirtualizedList = memo<VirtualizedListProps>(({ mobile }) => {
|
||||
// increaseViewportBy={overscan}
|
||||
initialTopMostItemIndex={data?.length - 1}
|
||||
isScrolling={setIsScrolling}
|
||||
itemContent={itemContent}
|
||||
itemContent={(index, id) => <ItemContent id={id} index={index} mobile={mobile} />}
|
||||
overscan={overscan}
|
||||
ref={virtuosoRef}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
import { ReactNode, Suspense, lazy, memo } from 'react';
|
||||
import { ReactNode, Suspense, lazy } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import ChatHydration from '@/components/StoreHydration/ChatHydration';
|
||||
@@ -8,26 +7,12 @@ import SkeletonList from './components/SkeletonList';
|
||||
|
||||
const ChatList = lazy(() => import('./components/VirtualizedList'));
|
||||
|
||||
const useStyles = createStyles(
|
||||
({ css, responsive }) => css`
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
|
||||
${responsive.mobile} {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
);
|
||||
|
||||
interface ConversationProps {
|
||||
chatInput: ReactNode;
|
||||
mobile?: boolean;
|
||||
}
|
||||
|
||||
const Conversation = memo<ConversationProps>(({ chatInput, mobile }) => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
const Conversation = ({ chatInput, mobile }: ConversationProps) => {
|
||||
return (
|
||||
<Flexbox
|
||||
flex={1}
|
||||
@@ -35,15 +20,23 @@ const Conversation = memo<ConversationProps>(({ chatInput, mobile }) => {
|
||||
// `relative` is required, ChatInput's absolute position needs it
|
||||
style={{ position: 'relative' }}
|
||||
>
|
||||
<div className={styles}>
|
||||
<Flexbox
|
||||
height={'100%'}
|
||||
style={{
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto',
|
||||
position: 'relative',
|
||||
}}
|
||||
width={'100%'}
|
||||
>
|
||||
<Suspense fallback={<SkeletonList mobile={mobile} />}>
|
||||
<ChatList mobile={mobile} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</Flexbox>
|
||||
{chatInput}
|
||||
<ChatHydration />
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default Conversation;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { DraggablePanel, DraggablePanelContainer } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { createStyles, useResponsive } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { PropsWithChildren, memo, useState } from 'react';
|
||||
import { PropsWithChildren, memo, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { FOLDER_WIDTH } from '@/const/layoutTokens';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
@@ -15,6 +17,7 @@ export const useStyles = createStyles(({ css, token }) => ({
|
||||
}));
|
||||
|
||||
const FolderPanel = memo<PropsWithChildren>(({ children }) => {
|
||||
const { md = true } = useResponsive();
|
||||
const { styles } = useStyles();
|
||||
const [sessionsWidth, sessionExpandable, updatePreference] = useGlobalStore((s) => [
|
||||
s.preference.sessionsWidth,
|
||||
@@ -24,6 +27,17 @@ const FolderPanel = memo<PropsWithChildren>(({ children }) => {
|
||||
const [tmpWidth, setWidth] = useState(sessionsWidth);
|
||||
if (tmpWidth !== sessionsWidth) setWidth(sessionsWidth);
|
||||
|
||||
const handleExpand = (expand: boolean) => {
|
||||
updatePreference({
|
||||
sessionsWidth: expand ? 320 : 0,
|
||||
showSessionPanel: expand,
|
||||
});
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
handleExpand(md);
|
||||
}, [md]);
|
||||
|
||||
return (
|
||||
<DraggablePanel
|
||||
className={styles.panel}
|
||||
@@ -31,12 +45,8 @@ const FolderPanel = memo<PropsWithChildren>(({ children }) => {
|
||||
expand={sessionExpandable}
|
||||
maxWidth={400}
|
||||
minWidth={FOLDER_WIDTH}
|
||||
onExpandChange={(expand) => {
|
||||
updatePreference({
|
||||
sessionsWidth: expand ? 320 : 0,
|
||||
showSessionPanel: expand,
|
||||
});
|
||||
}}
|
||||
mode={md ? 'fixed' : 'float'}
|
||||
onExpandChange={handleExpand}
|
||||
onSizeChange={(_, size) => {
|
||||
if (!size) return;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user