@lobehub/chat 1.123.3 → 1.124.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/chat.json +6 -2
  4. package/locales/ar/editor.json +47 -0
  5. package/locales/bg-BG/chat.json +6 -2
  6. package/locales/bg-BG/editor.json +47 -0
  7. package/locales/de-DE/chat.json +6 -2
  8. package/locales/de-DE/editor.json +47 -0
  9. package/locales/en-US/chat.json +6 -2
  10. package/locales/en-US/editor.json +47 -0
  11. package/locales/es-ES/chat.json +6 -2
  12. package/locales/es-ES/editor.json +47 -0
  13. package/locales/es-ES/models.json +3 -1
  14. package/locales/fa-IR/chat.json +6 -2
  15. package/locales/fa-IR/editor.json +47 -0
  16. package/locales/fr-FR/chat.json +6 -2
  17. package/locales/fr-FR/editor.json +47 -0
  18. package/locales/it-IT/chat.json +6 -2
  19. package/locales/it-IT/editor.json +47 -0
  20. package/locales/ja-JP/chat.json +6 -2
  21. package/locales/ja-JP/editor.json +47 -0
  22. package/locales/ko-KR/chat.json +6 -2
  23. package/locales/ko-KR/editor.json +47 -0
  24. package/locales/ko-KR/models.json +3 -1
  25. package/locales/nl-NL/chat.json +6 -2
  26. package/locales/nl-NL/editor.json +47 -0
  27. package/locales/nl-NL/models.json +3 -1
  28. package/locales/pl-PL/chat.json +6 -2
  29. package/locales/pl-PL/editor.json +47 -0
  30. package/locales/pt-BR/chat.json +6 -2
  31. package/locales/pt-BR/editor.json +47 -0
  32. package/locales/ru-RU/chat.json +6 -2
  33. package/locales/ru-RU/editor.json +47 -0
  34. package/locales/tr-TR/chat.json +6 -2
  35. package/locales/tr-TR/editor.json +47 -0
  36. package/locales/vi-VN/chat.json +6 -2
  37. package/locales/vi-VN/editor.json +47 -0
  38. package/locales/zh-CN/chat.json +6 -2
  39. package/locales/zh-CN/editor.json +47 -0
  40. package/locales/zh-TW/chat.json +6 -2
  41. package/locales/zh-TW/editor.json +47 -0
  42. package/locales/zh-TW/models.json +3 -1
  43. package/next.config.ts +4 -0
  44. package/package.json +4 -2
  45. package/packages/const/src/layoutTokens.ts +1 -0
  46. package/packages/types/src/index.ts +1 -0
  47. package/packages/utils/src/index.ts +1 -0
  48. package/src/app/(backend)/webapi/chat/[provider]/route.ts +1 -1
  49. package/src/app/(backend)/webapi/chat/vertexai/route.ts +1 -0
  50. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/{Footer/MessageFromUrl.tsx → MessageFromUrl.tsx} +3 -2
  51. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/index.tsx +129 -28
  52. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/index.tsx +44 -66
  53. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/useSend.ts +141 -0
  54. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/Content.tsx +7 -1
  55. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/QuestionSuggest.tsx +3 -2
  56. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/OpeningQuestions.tsx +3 -2
  57. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/HeaderAction.tsx +18 -2
  58. package/src/features/ChatInput/ActionBar/STT/common.tsx +41 -47
  59. package/src/features/ChatInput/{Topic → ActionBar/SaveTopic}/index.tsx +15 -4
  60. package/src/features/ChatInput/ActionBar/Typo/index.tsx +22 -0
  61. package/src/features/ChatInput/ActionBar/components/Action.tsx +4 -0
  62. package/src/features/ChatInput/ActionBar/config.ts +7 -1
  63. package/src/features/ChatInput/ActionBar/index.tsx +40 -51
  64. package/src/features/ChatInput/ChatInputProvider.tsx +54 -0
  65. package/src/features/ChatInput/Desktop/FilePreview/FileItem/index.tsx +20 -11
  66. package/src/features/ChatInput/Desktop/FilePreview/FileList.tsx +16 -15
  67. package/src/features/ChatInput/Desktop/index.tsx +81 -68
  68. package/src/features/ChatInput/InputEditor/index.tsx +134 -0
  69. package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/File.tsx +1 -2
  70. package/src/features/ChatInput/Mobile/FilePreview/index.tsx +44 -0
  71. package/src/features/ChatInput/Mobile/index.tsx +72 -0
  72. package/src/features/ChatInput/SendArea/ExpandButton.tsx +30 -0
  73. package/src/features/ChatInput/SendArea/SendButton.tsx +29 -0
  74. package/src/features/ChatInput/SendArea/ShortcutHint.tsx +52 -0
  75. package/src/features/ChatInput/SendArea/index.tsx +36 -0
  76. package/src/features/ChatInput/StoreUpdater.tsx +41 -0
  77. package/src/features/ChatInput/TypoBar/index.tsx +139 -0
  78. package/src/features/ChatInput/hooks/useChatInputEditor.ts +36 -0
  79. package/src/features/ChatInput/index.ts +7 -0
  80. package/src/features/ChatInput/store/action.ts +75 -0
  81. package/src/features/ChatInput/store/index.ts +23 -0
  82. package/src/features/ChatInput/store/initialState.ts +54 -0
  83. package/src/features/ChatInput/store/selectors.ts +5 -0
  84. package/src/features/Conversation/components/BackBottom/style.ts +1 -1
  85. package/src/features/Conversation/components/SkeletonList.tsx +10 -3
  86. package/src/features/Conversation/components/VirtualizedList/index.tsx +53 -44
  87. package/src/features/Conversation/components/WideScreenContainer/index.tsx +43 -0
  88. package/src/features/Portal/Thread/Chat/ChatInput/index.tsx +49 -42
  89. package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +48 -22
  90. package/src/features/Portal/Thread/Chat/index.tsx +2 -2
  91. package/src/features/Portal/Thread/Header/index.tsx +1 -1
  92. package/src/hooks/useHotkeys/chatScope.ts +5 -3
  93. package/src/layout/GlobalProvider/Editor.tsx +27 -0
  94. package/src/layout/GlobalProvider/Locale.tsx +3 -23
  95. package/src/libs/trpc/client/lambda.ts +76 -63
  96. package/src/locales/default/chat.ts +7 -2
  97. package/src/locales/default/editor.ts +47 -0
  98. package/src/locales/default/index.ts +2 -0
  99. package/src/services/aiChat.ts +8 -2
  100. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +42 -33
  101. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +174 -35
  102. package/src/store/chat/slices/aiChat/initialState.ts +19 -0
  103. package/src/store/chat/slices/aiChat/selectors.ts +18 -0
  104. package/src/store/global/action.test.ts +6 -5
  105. package/src/store/global/actions/__tests__/general.test.ts +6 -6
  106. package/src/store/global/actions/workspacePane.ts +6 -0
  107. package/src/store/global/initialState.ts +2 -4
  108. package/src/store/global/selectors/systemStatus.test.ts +1 -2
  109. package/src/store/global/selectors/systemStatus.ts +2 -5
  110. package/src/app/(backend)/webapi/chat/anthropic/route.test.ts +0 -30
  111. package/src/app/(backend)/webapi/chat/anthropic/route.ts +0 -21
  112. package/src/app/(backend)/webapi/chat/google/route.test.ts +0 -35
  113. package/src/app/(backend)/webapi/chat/google/route.ts +0 -25
  114. package/src/app/(backend)/webapi/chat/groq/route.test.ts +0 -29
  115. package/src/app/(backend)/webapi/chat/groq/route.ts +0 -21
  116. package/src/app/(backend)/webapi/chat/openai/route.test.ts +0 -30
  117. package/src/app/(backend)/webapi/chat/openai/route.ts +0 -26
  118. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/SendMore.tsx +0 -104
  119. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/ShortcutHint.tsx +0 -40
  120. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +0 -125
  121. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.test.tsx +0 -332
  122. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.tsx +0 -29
  123. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files/index.tsx +0 -33
  124. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/Container.tsx +0 -41
  125. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/index.tsx +0 -156
  126. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Send.tsx +0 -33
  127. package/src/features/ChatInput/Desktop/Header/index.tsx +0 -30
  128. package/src/features/ChatInput/Desktop/InputArea/index.tsx +0 -143
  129. package/src/features/ChatInput/Desktop/__tests__/useAutoFocus.test.ts +0 -45
  130. package/src/features/ChatInput/Desktop/useAutoFocus.ts +0 -13
  131. package/src/features/ChatInput/useSend.ts +0 -102
  132. package/src/features/Portal/Thread/Chat/ChatInput/Footer.tsx +0 -90
  133. package/src/features/Portal/Thread/Chat/ChatInput/TextArea.tsx +0 -30
  134. package/src/libs/trpc/client/types.ts +0 -18
  135. /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/Image.tsx +0 -0
  136. /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/index.tsx +0 -0
  137. /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/style.ts +0 -0
@@ -0,0 +1,139 @@
1
+ import { useEditorState } from '@lobehub/editor/react';
2
+ import {
3
+ ChatInputActionBar,
4
+ ChatInputActions,
5
+ type ChatInputActionsProps,
6
+ CodeLanguageSelect,
7
+ } from '@lobehub/editor/react';
8
+ import { useTheme } from 'antd-style';
9
+ import {
10
+ BoldIcon,
11
+ CodeXmlIcon,
12
+ ItalicIcon,
13
+ LinkIcon,
14
+ ListIcon,
15
+ ListOrderedIcon,
16
+ MessageSquareQuote,
17
+ SquareDashedBottomCodeIcon,
18
+ StrikethroughIcon,
19
+ } from 'lucide-react';
20
+ import { memo } from 'react';
21
+ import { useTranslation } from 'react-i18next';
22
+
23
+ import { useChatInputStore } from '@/features/ChatInput/store';
24
+
25
+ const TypoBar = memo(() => {
26
+ const { t } = useTranslation('editor');
27
+ const editor = useChatInputStore((s) => s.editor);
28
+ const editorState = useEditorState(editor);
29
+ const theme = useTheme();
30
+
31
+ return (
32
+ <ChatInputActionBar
33
+ left={
34
+ <ChatInputActions
35
+ items={
36
+ [
37
+ {
38
+ active: editorState.isBold,
39
+ icon: BoldIcon,
40
+ key: 'bold',
41
+ label: t('typobar.bold'),
42
+ onClick: editorState.bold,
43
+ },
44
+ {
45
+ active: editorState.isItalic,
46
+ icon: ItalicIcon,
47
+ key: 'italic',
48
+ label: t('typobar.italic'),
49
+ onClick: editorState.italic,
50
+ },
51
+ // TODO: 目前 markdown 不支持 <u>
52
+ // {
53
+ // active: editorState.isUnderline,
54
+ // icon: UnderlineIcon,
55
+ // key: 'underline',
56
+ // label: t('typobar.underline'),
57
+ // onClick: editorState.underline,
58
+ // },
59
+ {
60
+ active: editorState.isStrikethrough,
61
+ icon: StrikethroughIcon,
62
+ key: 'strikethrough',
63
+ label: t('typobar.strikethrough'),
64
+ onClick: editorState.strikethrough,
65
+ },
66
+ {
67
+ type: 'divider',
68
+ },
69
+
70
+ {
71
+ icon: ListIcon,
72
+ key: 'bulletList',
73
+ label: t('typobar.bulletList'),
74
+ onClick: editorState.bulletList,
75
+ },
76
+ {
77
+ icon: ListOrderedIcon,
78
+ key: 'numberlist',
79
+ label: t('typobar.numberList'),
80
+ onClick: editorState.numberList,
81
+ },
82
+ {
83
+ active: editorState.isBlockquote,
84
+ icon: MessageSquareQuote,
85
+ key: 'blockquote',
86
+ label: t('typobar.blockquote'),
87
+ onClick: editorState.blockquote,
88
+ },
89
+ {
90
+ icon: LinkIcon,
91
+ key: 'link',
92
+ label: t('typobar.link'),
93
+ onClick: editorState.insertLink,
94
+ },
95
+ {
96
+ type: 'divider',
97
+ },
98
+ {
99
+ active: editorState.isCode,
100
+ icon: CodeXmlIcon,
101
+ key: 'code',
102
+ label: t('typobar.code'),
103
+ onClick: editorState.code,
104
+ },
105
+ {
106
+ icon: SquareDashedBottomCodeIcon,
107
+ key: 'codeblock',
108
+ label: t('typobar.codeblock'),
109
+ onClick: editorState.codeblock,
110
+ },
111
+ editorState.isCodeblock && {
112
+ children: (
113
+ <CodeLanguageSelect
114
+ onSelect={(value) => editorState.updateCodeblockLang(value)}
115
+ value={editorState.codeblockLang}
116
+ />
117
+ ),
118
+ disabled: !editorState.isCodeblock,
119
+ key: 'codeblockLang',
120
+ },
121
+ ].filter(Boolean) as ChatInputActionsProps['items']
122
+ }
123
+ onClick={() => {
124
+ editor?.focus();
125
+ }}
126
+ />
127
+ }
128
+ style={{
129
+ background: theme.colorFillQuaternary,
130
+ borderTopLeftRadius: 8,
131
+ borderTopRightRadius: 8,
132
+ }}
133
+ />
134
+ );
135
+ });
136
+
137
+ TypoBar.displayName = 'TypoBar';
138
+
139
+ export default TypoBar;
@@ -0,0 +1,36 @@
1
+ import { IEditor } from '@lobehub/editor';
2
+ import { useMemo } from 'react';
3
+
4
+ import { useChatInputStore } from '@/features/ChatInput/store';
5
+
6
+ export interface ChatInputEditor {
7
+ clearContent: () => void;
8
+ focus: () => void;
9
+ getJSONState: () => any;
10
+ getMarkdownContent: () => string;
11
+ instance: IEditor;
12
+ setExpand: (expand: boolean) => void;
13
+ setJSONState: (content: any) => void;
14
+ }
15
+ export const useChatInputEditor = () => {
16
+ const [editor, getMarkdownContent, getJSONState, setExpand, setJSONState] = useChatInputStore(
17
+ (s) => [s.editor, s.getMarkdownContent, s.getJSONState, s.setExpand, s.setJSONState],
18
+ );
19
+
20
+ return useMemo<ChatInputEditor>(
21
+ () => ({
22
+ clearContent: () => {
23
+ editor?.cleanDocument();
24
+ },
25
+ focus: () => {
26
+ editor?.focus();
27
+ },
28
+ getJSONState,
29
+ getMarkdownContent,
30
+ instance: editor!,
31
+ setExpand,
32
+ setJSONState,
33
+ }),
34
+ [editor],
35
+ );
36
+ };
@@ -0,0 +1,7 @@
1
+ export type { ActionKey, ActionKeys } from './ActionBar/config';
2
+ export { ChatInputProvider } from './ChatInputProvider';
3
+ export { default as DesktopChatInput } from './Desktop';
4
+ export type { ChatInputEditor } from './hooks/useChatInputEditor';
5
+ export { useChatInputEditor } from './hooks/useChatInputEditor';
6
+ export { default as MobileChatInput } from './Mobile';
7
+ export type { SendButtonHandler } from './store/initialState';
@@ -0,0 +1,75 @@
1
+ import { StateCreator } from 'zustand/vanilla';
2
+
3
+ import { PublicState, State, initialState } from './initialState';
4
+
5
+ export interface Action {
6
+ getJSONState: () => any;
7
+ getMarkdownContent: () => string;
8
+ handleSendButton: () => void;
9
+ handleStop: () => void;
10
+ setExpand: (expend: boolean) => void;
11
+ setJSONState: (content: any) => void;
12
+ setShowTypoBar: (show: boolean) => void;
13
+ updateMarkdownContent: () => void;
14
+ }
15
+
16
+ export type Store = Action & State;
17
+
18
+ // const t = setNamespace('ChatInput');
19
+
20
+ type CreateStore = (
21
+ initState?: Partial<PublicState>,
22
+ ) => StateCreator<Store, [['zustand/devtools', never]]>;
23
+
24
+ export const store: CreateStore = (publicState) => (set, get) => ({
25
+ ...initialState,
26
+ ...publicState,
27
+
28
+ getJSONState: () => {
29
+ return get().editor?.getDocument('json');
30
+ },
31
+ getMarkdownContent: () => {
32
+ return String(get().editor?.getDocument('markdown') || '').trimEnd();
33
+ },
34
+ handleSendButton: () => {
35
+ if (!get().editor) return;
36
+
37
+ const editor = get().editor;
38
+
39
+ get().onSend?.({
40
+ clearContent: () => editor?.cleanDocument(),
41
+ editor: editor!,
42
+ getMarkdownContent: get().getMarkdownContent,
43
+ });
44
+ },
45
+
46
+ handleStop: () => {
47
+ if (!get().editor) return;
48
+
49
+ get().sendButtonProps?.onStop?.({ editor: get().editor! });
50
+ },
51
+
52
+ setExpand: (expand) => {
53
+ set({ expand });
54
+ },
55
+
56
+ setJSONState: (content) => {
57
+ get().editor?.setDocument('json', content);
58
+ },
59
+
60
+ setShowTypoBar: (showTypoBar) => {
61
+ set({ showTypoBar });
62
+ },
63
+
64
+ updateMarkdownContent: () => {
65
+ if (!get().onMarkdownContentChange) return;
66
+
67
+ const content = get().getMarkdownContent();
68
+
69
+ if (content === get().markdownContent) return;
70
+
71
+ get().onMarkdownContentChange?.(content);
72
+
73
+ set({ markdownContent: content });
74
+ },
75
+ });
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+
3
+ import { StoreApiWithSelector } from '@lobechat/types';
4
+ import { createContext } from 'zustand-utils';
5
+ import { subscribeWithSelector } from 'zustand/middleware';
6
+ import { shallow } from 'zustand/shallow';
7
+ import { createWithEqualityFn } from 'zustand/traditional';
8
+
9
+ import { Store, store } from './action';
10
+ import { State } from './initialState';
11
+
12
+ export type { PublicState, State } from './initialState';
13
+
14
+ export const createStore = (initState?: Partial<State>) =>
15
+ createWithEqualityFn(subscribeWithSelector(store(initState)), shallow);
16
+
17
+ export const {
18
+ useStore: useChatInputStore,
19
+ useStoreApi,
20
+ Provider,
21
+ } = createContext<StoreApiWithSelector<Store>>();
22
+
23
+ export { selectors } from './selectors';
@@ -0,0 +1,54 @@
1
+ import type { IEditor } from '@lobehub/editor';
2
+ import type { ChatInputProps } from '@lobehub/editor/react';
3
+ import type { MenuProps } from '@lobehub/ui/es/Menu';
4
+
5
+ import { ActionKeys } from '@/features/ChatInput';
6
+
7
+ export type SendButtonHandler = (params: {
8
+ clearContent: () => void;
9
+ editor: IEditor;
10
+ getMarkdownContent: () => string;
11
+ }) => Promise<void> | void;
12
+
13
+ export interface SendButtonProps {
14
+ disabled?: boolean;
15
+ generating: boolean;
16
+ onStop: (params: { editor: IEditor }) => void;
17
+ shape?: 'round' | 'default';
18
+ }
19
+
20
+ export const initialSendButtonState: SendButtonProps = {
21
+ disabled: false,
22
+ generating: false,
23
+ onStop: () => {},
24
+ };
25
+
26
+ export interface PublicState {
27
+ allowExpand?: boolean;
28
+ expand?: boolean;
29
+ leftActions: ActionKeys[];
30
+ mobile?: boolean;
31
+ onMarkdownContentChange?: (content: string) => void;
32
+ onSend?: SendButtonHandler;
33
+ rightActions: ActionKeys[];
34
+ sendButtonProps?: SendButtonProps;
35
+ sendMenu?: MenuProps;
36
+ showTypoBar?: boolean;
37
+ }
38
+
39
+ export interface State extends PublicState {
40
+ editor?: IEditor;
41
+ isContentEmpty: boolean;
42
+ markdownContent: string;
43
+ slashMenuRef: ChatInputProps['slashMenuRef'];
44
+ }
45
+
46
+ export const initialState: State = {
47
+ allowExpand: true,
48
+ expand: false,
49
+ isContentEmpty: false,
50
+ leftActions: [],
51
+ markdownContent: '',
52
+ rightActions: [],
53
+ slashMenuRef: { current: null },
54
+ };
@@ -0,0 +1,5 @@
1
+ import { SendButtonProps, State, initialSendButtonState } from './initialState';
2
+
3
+ export const selectors = {
4
+ sendButtonProps: (s: State): SendButtonProps => s.sendButtonProps || initialSendButtonState,
5
+ };
@@ -8,7 +8,7 @@ export const useStyles = createStyles(({ token, css, stylish, cx, responsive })
8
8
  pointer-events: none;
9
9
 
10
10
  position: absolute;
11
- z-index: 1000;
11
+ z-index: 50;
12
12
  inset-block-end: 16px;
13
13
  inset-inline-end: 16px;
14
14
  transform: translateY(16px);
@@ -3,7 +3,8 @@
3
3
  import { Skeleton } from 'antd';
4
4
  import { createStyles } from 'antd-style';
5
5
  import { memo } from 'react';
6
- import { Flexbox } from 'react-layout-kit';
6
+
7
+ import WideScreenContainer from '@/features/Conversation/components/WideScreenContainer';
7
8
 
8
9
  const useStyles = createStyles(({ css, prefixCls }) => ({
9
10
  message: css`
@@ -30,7 +31,13 @@ const SkeletonList = memo<SkeletonListProps>(({ mobile }) => {
30
31
  const { cx, styles } = useStyles();
31
32
 
32
33
  return (
33
- <Flexbox gap={24} padding={mobile ? 8 : 12} style={{ marginTop: 24 }}>
34
+ <WideScreenContainer
35
+ flex={1}
36
+ gap={24}
37
+ height={'100%'}
38
+ padding={mobile ? 8 : 12}
39
+ style={{ marginTop: 24 }}
40
+ >
34
41
  <Skeleton
35
42
  active
36
43
  avatar={{ size: mobile ? 32 : 40 }}
@@ -45,7 +52,7 @@ const SkeletonList = memo<SkeletonListProps>(({ mobile }) => {
45
52
  paragraph={{ width: mobile ? ['80%', '40%'] : ['50%', '30%'] }}
46
53
  title={false}
47
54
  />
48
- </Flexbox>
55
+ </WideScreenContainer>
49
56
  );
50
57
  });
51
58
  export default SkeletonList;
@@ -1,13 +1,10 @@
1
1
  'use client';
2
2
 
3
- import { Icon } from '@lobehub/ui';
4
- import { useTheme } from 'antd-style';
5
- import { Loader2Icon } from 'lucide-react';
6
- import React, { ReactNode, memo, useCallback, useEffect, useRef, useState } from 'react';
7
- import { Center, Flexbox } from 'react-layout-kit';
3
+ import { ReactNode, forwardRef, memo, useCallback, useEffect, useRef, useState } from 'react';
4
+ import { Flexbox } from 'react-layout-kit';
8
5
  import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
9
6
 
10
- import { isServerMode } from '@/const/version';
7
+ import WideScreenContainer from '@/features/Conversation/components/WideScreenContainer';
11
8
  import { useChatStore } from '@/store/chat';
12
9
  import { chatSelectors } from '@/store/chat/selectors';
13
10
 
@@ -21,8 +18,17 @@ interface VirtualizedListProps {
21
18
  mobile?: boolean;
22
19
  }
23
20
 
21
+ const List = forwardRef(({ ...props }, ref) => {
22
+ return (
23
+ <Flexbox>
24
+ <WideScreenContainer id={'chatlist-list'} ref={ref} {...props} />
25
+ </Flexbox>
26
+ );
27
+ });
28
+
24
29
  const VirtualizedList = memo<VirtualizedListProps>(({ mobile, dataSource, itemContent }) => {
25
30
  const virtuosoRef = useRef<VirtuosoHandle>(null);
31
+ const prevDataLengthRef = useRef(dataSource.length);
26
32
  const [atBottom, setAtBottom] = useState(true);
27
33
  const [isScrolling, setIsScrolling] = useState(false);
28
34
 
@@ -32,71 +38,74 @@ const VirtualizedList = memo<VirtualizedListProps>(({ mobile, dataSource, itemCo
32
38
  chatSelectors.isCurrentChatLoaded(s),
33
39
  ]);
34
40
 
35
- useEffect(() => {
36
- if (virtuosoRef.current) {
37
- virtuosoRef.current.scrollToIndex({ align: 'end', behavior: 'auto', index: 'LAST' });
38
- }
39
- }, [id]);
40
-
41
- const prevDataLengthRef = useRef(dataSource.length);
42
-
43
41
  const getFollowOutput = useCallback(() => {
44
42
  const newFollowOutput = dataSource.length > prevDataLengthRef.current ? 'auto' : false;
45
43
  prevDataLengthRef.current = dataSource.length;
46
44
  return newFollowOutput;
47
45
  }, [dataSource.length]);
48
46
 
49
- const theme = useTheme();
47
+ const scrollToBottom = useCallback(
48
+ (behavior: 'auto' | 'smooth' = 'smooth') => {
49
+ if (atBottom) return;
50
+ if (!virtuosoRef.current) return;
51
+ virtuosoRef.current.scrollToIndex({ align: 'end', behavior, index: 'LAST' });
52
+ },
53
+ [atBottom],
54
+ );
55
+
56
+ useEffect(() => {
57
+ scrollToBottom();
58
+ }, [id]);
59
+
50
60
  // overscan should be 3 times the height of the window
51
61
  const overscan = typeof window !== 'undefined' ? window.innerHeight * 3 : 0;
52
62
 
53
63
  // first time loading or not loaded
54
- if (isFirstLoading) return <SkeletonList mobile={mobile} />;
55
-
56
- if (!isCurrentChatLoaded)
57
- // use skeleton list when not loaded in server mode due to the loading duration is much longer than client mode
58
- return isServerMode ? (
59
- <SkeletonList mobile={mobile} />
60
- ) : (
61
- // in client mode and switch page, using the center loading for smooth transition
62
- <Center height={'100%'} width={'100%'}>
63
- <Icon icon={Loader2Icon} size={32} spin style={{ color: theme.colorTextTertiary }} />
64
- </Center>
65
- );
64
+ if (isFirstLoading || !isCurrentChatLoaded) return <SkeletonList mobile={mobile} />;
66
65
 
67
66
  return (
68
67
  <VirtuosoContext value={virtuosoRef}>
69
- <Flexbox height={'100%'}>
70
- <Virtuoso
71
- atBottomStateChange={setAtBottom}
72
- atBottomThreshold={50 * (mobile ? 2 : 1)}
73
- computeItemKey={(_, item) => item}
74
- data={dataSource}
75
- followOutput={getFollowOutput}
76
- increaseViewportBy={overscan}
77
- initialTopMostItemIndex={dataSource?.length - 1}
78
- isScrolling={setIsScrolling}
79
- itemContent={itemContent}
80
- ref={virtuosoRef}
81
- />
68
+ <Virtuoso
69
+ atBottomStateChange={setAtBottom}
70
+ atBottomThreshold={50 * (mobile ? 2 : 1)}
71
+ components={{
72
+ List,
73
+ }}
74
+ computeItemKey={(_, item) => item}
75
+ data={dataSource}
76
+ followOutput={getFollowOutput}
77
+ increaseViewportBy={overscan}
78
+ initialTopMostItemIndex={dataSource?.length - 1}
79
+ isScrolling={setIsScrolling}
80
+ itemContent={itemContent}
81
+ ref={virtuosoRef}
82
+ />
83
+ <WideScreenContainer
84
+ onChange={() => {
85
+ if (!atBottom) return;
86
+ setTimeout(scrollToBottom, 100);
87
+ }}
88
+ style={{
89
+ position: 'relative',
90
+ }}
91
+ >
82
92
  <AutoScroll
83
93
  atBottom={atBottom}
84
94
  isScrolling={isScrolling}
85
95
  onScrollToBottom={(type) => {
86
- const virtuoso = virtuosoRef.current;
87
96
  switch (type) {
88
97
  case 'auto': {
89
- virtuoso?.scrollToIndex({ align: 'end', behavior: 'auto', index: 'LAST' });
98
+ scrollToBottom();
90
99
  break;
91
100
  }
92
101
  case 'click': {
93
- virtuoso?.scrollToIndex({ align: 'end', behavior: 'smooth', index: 'LAST' });
102
+ scrollToBottom('smooth');
94
103
  break;
95
104
  }
96
105
  }
97
106
  }}
98
107
  />
99
- </Flexbox>
108
+ </WideScreenContainer>
100
109
  </VirtuosoContext>
101
110
  );
102
111
  });
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+
3
+ import { createStyles } from 'antd-style';
4
+ import { memo, useEffect } from 'react';
5
+ import { Flexbox, FlexboxProps } from 'react-layout-kit';
6
+
7
+ import { CONVERSATION_MIN_WIDTH } from '@/const/layoutTokens';
8
+ import { useGlobalStore } from '@/store/global';
9
+ import { systemStatusSelectors } from '@/store/global/selectors';
10
+
11
+ const useStyles = createStyles(({ css, token }) => ({
12
+ container: css`
13
+ align-self: center;
14
+ transition: width 0.25s ${token.motionEaseInOut};
15
+ `,
16
+ }));
17
+
18
+ interface WideScreenContainerProps extends FlexboxProps {
19
+ onChange?: () => void;
20
+ }
21
+
22
+ const WideScreenContainer = memo<WideScreenContainerProps>(
23
+ ({ children, className, onChange, ...rest }) => {
24
+ const { cx, styles } = useStyles();
25
+ const wideScreen = useGlobalStore(systemStatusSelectors.wideScreen);
26
+
27
+ useEffect(() => {
28
+ onChange?.();
29
+ }, [wideScreen]);
30
+
31
+ return (
32
+ <Flexbox
33
+ className={cx(styles.container, className)}
34
+ width={wideScreen ? '100%' : `min(${CONVERSATION_MIN_WIDTH}px, 100%)`}
35
+ {...rest}
36
+ >
37
+ {children}
38
+ </Flexbox>
39
+ );
40
+ },
41
+ );
42
+
43
+ export default WideScreenContainer;