@lobehub/chat 1.123.4 → 1.124.1

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 (149) hide show
  1. package/.env.example +5 -0
  2. package/CHANGELOG.md +58 -0
  3. package/Dockerfile +2 -0
  4. package/Dockerfile.database +2 -0
  5. package/Dockerfile.pglite +2 -0
  6. package/changelog/v1.json +21 -0
  7. package/docs/self-hosting/environment-variables/model-provider.mdx +18 -0
  8. package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +20 -0
  9. package/locales/ar/chat.json +8 -2
  10. package/locales/ar/editor.json +47 -0
  11. package/locales/bg-BG/chat.json +8 -2
  12. package/locales/bg-BG/editor.json +47 -0
  13. package/locales/de-DE/chat.json +8 -2
  14. package/locales/de-DE/editor.json +47 -0
  15. package/locales/en-US/chat.json +8 -2
  16. package/locales/en-US/editor.json +47 -0
  17. package/locales/es-ES/chat.json +8 -2
  18. package/locales/es-ES/editor.json +47 -0
  19. package/locales/es-ES/models.json +3 -1
  20. package/locales/fa-IR/chat.json +8 -2
  21. package/locales/fa-IR/editor.json +47 -0
  22. package/locales/fr-FR/chat.json +8 -2
  23. package/locales/fr-FR/editor.json +47 -0
  24. package/locales/it-IT/chat.json +8 -2
  25. package/locales/it-IT/editor.json +47 -0
  26. package/locales/ja-JP/chat.json +8 -2
  27. package/locales/ja-JP/editor.json +47 -0
  28. package/locales/ko-KR/chat.json +8 -2
  29. package/locales/ko-KR/editor.json +47 -0
  30. package/locales/ko-KR/models.json +3 -1
  31. package/locales/nl-NL/chat.json +8 -2
  32. package/locales/nl-NL/editor.json +47 -0
  33. package/locales/nl-NL/models.json +3 -1
  34. package/locales/pl-PL/chat.json +8 -2
  35. package/locales/pl-PL/editor.json +47 -0
  36. package/locales/pt-BR/chat.json +8 -2
  37. package/locales/pt-BR/editor.json +47 -0
  38. package/locales/ru-RU/chat.json +8 -2
  39. package/locales/ru-RU/editor.json +47 -0
  40. package/locales/tr-TR/chat.json +8 -2
  41. package/locales/tr-TR/editor.json +47 -0
  42. package/locales/vi-VN/chat.json +8 -2
  43. package/locales/vi-VN/editor.json +47 -0
  44. package/locales/zh-CN/chat.json +8 -2
  45. package/locales/zh-CN/editor.json +47 -0
  46. package/locales/zh-CN/modelProvider.json +1 -1
  47. package/locales/zh-TW/chat.json +8 -2
  48. package/locales/zh-TW/editor.json +47 -0
  49. package/locales/zh-TW/models.json +3 -1
  50. package/next.config.ts +4 -0
  51. package/package.json +4 -2
  52. package/packages/const/src/layoutTokens.ts +1 -0
  53. package/packages/model-bank/src/aiModels/aihubmix.ts +38 -4
  54. package/packages/model-bank/src/aiModels/groq.ts +26 -8
  55. package/packages/model-bank/src/aiModels/hunyuan.ts +3 -3
  56. package/packages/model-bank/src/aiModels/modelscope.ts +13 -2
  57. package/packages/model-bank/src/aiModels/moonshot.ts +25 -5
  58. package/packages/model-bank/src/aiModels/novita.ts +40 -9
  59. package/packages/model-bank/src/aiModels/openrouter.ts +0 -13
  60. package/packages/model-bank/src/aiModels/qwen.ts +62 -1
  61. package/packages/model-bank/src/aiModels/siliconcloud.ts +20 -0
  62. package/packages/model-bank/src/aiModels/volcengine.ts +141 -15
  63. package/packages/model-runtime/src/newapi/index.test.ts +49 -42
  64. package/packages/model-runtime/src/newapi/index.ts +124 -143
  65. package/packages/types/src/index.ts +1 -0
  66. package/packages/utils/src/index.ts +1 -0
  67. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/{Footer/MessageFromUrl.tsx → MessageFromUrl.tsx} +3 -2
  68. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/index.tsx +129 -28
  69. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/index.tsx +44 -66
  70. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/useSend.ts +141 -0
  71. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/Content.tsx +7 -1
  72. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/QuestionSuggest.tsx +3 -2
  73. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/OpeningQuestions.tsx +3 -2
  74. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/HeaderAction.tsx +18 -2
  75. package/src/app/[variants]/(main)/settings/provider/(detail)/newapi/page.tsx +1 -1
  76. package/src/config/llm.ts +8 -0
  77. package/src/features/ChatInput/ActionBar/STT/common.tsx +41 -47
  78. package/src/features/ChatInput/{Topic → ActionBar/SaveTopic}/index.tsx +15 -4
  79. package/src/features/ChatInput/ActionBar/Typo/index.tsx +22 -0
  80. package/src/features/ChatInput/ActionBar/components/Action.tsx +4 -0
  81. package/src/features/ChatInput/ActionBar/config.ts +7 -1
  82. package/src/features/ChatInput/ActionBar/index.tsx +40 -51
  83. package/src/features/ChatInput/ChatInputProvider.tsx +54 -0
  84. package/src/features/ChatInput/Desktop/FilePreview/FileItem/index.tsx +20 -11
  85. package/src/features/ChatInput/Desktop/FilePreview/FileList.tsx +16 -15
  86. package/src/features/ChatInput/Desktop/index.tsx +94 -69
  87. package/src/features/ChatInput/InputEditor/index.tsx +134 -0
  88. package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/File.tsx +1 -2
  89. package/src/features/ChatInput/Mobile/FilePreview/index.tsx +44 -0
  90. package/src/features/ChatInput/Mobile/index.tsx +72 -0
  91. package/src/features/ChatInput/SendArea/ExpandButton.tsx +30 -0
  92. package/src/features/ChatInput/SendArea/SendButton.tsx +29 -0
  93. package/src/features/ChatInput/SendArea/ShortcutHint.tsx +52 -0
  94. package/src/features/ChatInput/SendArea/index.tsx +36 -0
  95. package/src/features/ChatInput/StoreUpdater.tsx +41 -0
  96. package/src/features/ChatInput/TypoBar/index.tsx +139 -0
  97. package/src/features/ChatInput/hooks/useChatInputEditor.ts +36 -0
  98. package/src/features/ChatInput/index.ts +7 -0
  99. package/src/features/ChatInput/store/action.ts +75 -0
  100. package/src/features/ChatInput/store/index.ts +23 -0
  101. package/src/features/ChatInput/store/initialState.ts +54 -0
  102. package/src/features/ChatInput/store/selectors.ts +5 -0
  103. package/src/features/Conversation/components/BackBottom/style.ts +1 -1
  104. package/src/features/Conversation/components/SkeletonList.tsx +10 -3
  105. package/src/features/Conversation/components/VirtualizedList/index.tsx +53 -44
  106. package/src/features/Conversation/components/WideScreenContainer/index.tsx +43 -0
  107. package/src/features/Portal/Thread/Chat/ChatInput/index.tsx +49 -42
  108. package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +48 -22
  109. package/src/features/Portal/Thread/Chat/index.tsx +2 -2
  110. package/src/features/Portal/Thread/Header/index.tsx +1 -1
  111. package/src/hooks/useHotkeys/chatScope.ts +5 -3
  112. package/src/layout/GlobalProvider/Editor.tsx +27 -0
  113. package/src/layout/GlobalProvider/Locale.tsx +3 -23
  114. package/src/locales/default/chat.ts +8 -2
  115. package/src/locales/default/editor.ts +47 -0
  116. package/src/locales/default/index.ts +2 -0
  117. package/src/locales/default/modelProvider.ts +1 -1
  118. package/src/services/aiChat.ts +8 -2
  119. package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +107 -0
  120. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +394 -40
  121. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +175 -35
  122. package/src/store/chat/slices/aiChat/initialState.ts +19 -0
  123. package/src/store/chat/slices/aiChat/selectors.ts +18 -0
  124. package/src/store/global/action.test.ts +6 -5
  125. package/src/store/global/actions/__tests__/general.test.ts +6 -6
  126. package/src/store/global/actions/workspacePane.ts +6 -0
  127. package/src/store/global/initialState.ts +2 -4
  128. package/src/store/global/selectors/systemStatus.test.ts +1 -2
  129. package/src/store/global/selectors/systemStatus.ts +2 -5
  130. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/SendMore.tsx +0 -104
  131. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/ShortcutHint.tsx +0 -40
  132. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +0 -125
  133. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.test.tsx +0 -332
  134. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.tsx +0 -29
  135. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files/index.tsx +0 -33
  136. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/Container.tsx +0 -41
  137. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/index.tsx +0 -156
  138. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Send.tsx +0 -33
  139. package/src/features/ChatInput/Desktop/Header/index.tsx +0 -30
  140. package/src/features/ChatInput/Desktop/InputArea/index.tsx +0 -143
  141. package/src/features/ChatInput/Desktop/__tests__/useAutoFocus.test.ts +0 -45
  142. package/src/features/ChatInput/Desktop/useAutoFocus.ts +0 -13
  143. package/src/features/ChatInput/useSend.ts +0 -102
  144. package/src/features/Portal/Thread/Chat/ChatInput/Footer.tsx +0 -90
  145. package/src/features/Portal/Thread/Chat/ChatInput/TextArea.tsx +0 -30
  146. package/src/libs/trpc/client/types.ts +0 -18
  147. /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/Image.tsx +0 -0
  148. /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/index.tsx +0 -0
  149. /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/style.ts +0 -0
@@ -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;
@@ -4,63 +4,70 @@ import { Alert } from '@lobehub/ui';
4
4
  import Link from 'next/link';
5
5
  import { memo } from 'react';
6
6
  import { Trans } from 'react-i18next';
7
+ import { Flexbox } from 'react-layout-kit';
7
8
 
8
- import { ActionKeys } from '@/features/ChatInput/ActionBar/config';
9
- import DesktopChatInput, { FooterRender } from '@/features/ChatInput/Desktop';
9
+ import { type ActionKeys, ChatInputProvider, DesktopChatInput } from '@/features/ChatInput';
10
+ import WideScreenContainer from '@/features/Conversation/components/WideScreenContainer';
11
+ import { useChatStore } from '@/store/chat';
10
12
  import { useGlobalStore } from '@/store/global';
11
13
  import { systemStatusSelectors } from '@/store/global/selectors';
12
14
 
13
- import Footer from './Footer';
14
- import TextArea from './TextArea';
15
+ import { useSendThreadMessage } from './useSend';
15
16
 
16
- const leftActions = ['stt', 'portalToken'] as ActionKeys[];
17
-
18
- const rightActions = [] as ActionKeys[];
19
-
20
- const renderTextArea = (onSend: () => void) => <TextArea onSend={onSend} />;
21
- const renderFooter: FooterRender = (props) => <Footer {...props} />;
17
+ const threadActions: ActionKeys[] = ['typo', 'stt', 'portalToken'];
22
18
 
23
19
  const Desktop = memo(() => {
24
- const [inputHeight, hideThreadLimitAlert, updateSystemStatus] = useGlobalStore((s) => [
25
- systemStatusSelectors.threadInputHeight(s),
20
+ const [hideThreadLimitAlert, updateSystemStatus] = useGlobalStore((s) => [
26
21
  systemStatusSelectors.systemStatus(s).hideThreadLimitAlert,
27
22
  s.updateSystemStatus,
28
23
  ]);
29
24
 
25
+ const { send, disabled, generating, stop } = useSendThreadMessage();
26
+
30
27
  return (
31
- <>
28
+ <WideScreenContainer>
32
29
  {!hideThreadLimitAlert && (
33
- <Alert
34
- banner
35
- closable
36
- message={
37
- <Trans i18nKey={'notSupportMultiModals'} ns={'thread'}>
38
- 子话题暂不支持文件/图片上传,如有需求,欢迎留言:
39
- <Link
40
- href={'https://github.com/lobehub/lobe-chat/discussions/4717'}
41
- style={{ textDecoration: 'underline' }}
42
- >
43
- 💬 讨论
44
- </Link>
45
- </Trans>
46
- }
47
- onClose={() => {
48
- updateSystemStatus({ hideThreadLimitAlert: true });
49
- }}
50
- type={'info'}
51
- />
30
+ <Flexbox paddingBlock={'0 6px'} paddingInline={12}>
31
+ <Alert
32
+ closable
33
+ message={
34
+ <Trans i18nKey={'notSupportMultiModals'} ns={'thread'}>
35
+ 子话题暂不支持文件/图片上传,如有需求,欢迎留言:
36
+ <Link
37
+ href={'https://github.com/lobehub/lobe-chat/discussions/4717'}
38
+ style={{ textDecoration: 'underline' }}
39
+ >
40
+ 💬 讨论
41
+ </Link>
42
+ </Trans>
43
+ }
44
+ onClose={() => {
45
+ updateSystemStatus({ hideThreadLimitAlert: true });
46
+ }}
47
+ type={'info'}
48
+ />
49
+ </Flexbox>
52
50
  )}
53
- <DesktopChatInput
54
- inputHeight={inputHeight}
55
- leftActions={leftActions}
56
- onInputHeightChange={(height) => {
57
- updateSystemStatus({ threadInputHeight: height });
51
+
52
+ <ChatInputProvider
53
+ chatInputEditorRef={(instance) => {
54
+ if (!instance) return;
55
+ useChatStore.setState({ threadInputEditor: instance });
56
+ }}
57
+ leftActions={threadActions}
58
+ onSend={() => {
59
+ send();
60
+ }}
61
+ sendButtonProps={{
62
+ disabled,
63
+ generating,
64
+ onStop: stop,
65
+ shape: 'round',
58
66
  }}
59
- renderFooter={renderFooter}
60
- renderTextArea={renderTextArea}
61
- rightActions={rightActions}
62
- />
63
- </>
67
+ >
68
+ <DesktopChatInput />
69
+ </ChatInputProvider>
70
+ </WideScreenContainer>
64
71
  );
65
72
  });
66
73
 
@@ -1,5 +1,8 @@
1
- import { useCallback, useMemo } from 'react';
1
+ import { useMemo, useState } from 'react';
2
2
 
3
+ import { useGeminiChineseWarning } from '@/hooks/useGeminiChineseWarning';
4
+ import { getAgentStoreState } from '@/store/agent';
5
+ import { agentSelectors } from '@/store/agent/slices/chat';
3
6
  import { useChatStore } from '@/store/chat';
4
7
  import { threadSelectors } from '@/store/chat/selectors';
5
8
  import { SendMessageParams } from '@/types/message';
@@ -10,41 +13,64 @@ export type UseSendMessageParams = Pick<
10
13
  >;
11
14
 
12
15
  export const useSendThreadMessage = () => {
16
+ const [loading, setLoading] = useState(false);
17
+ const canNotSend = useChatStore(threadSelectors.isSendButtonDisabledByMessage);
18
+ const generating = useChatStore((s) => threadSelectors.isThreadAIGenerating(s));
19
+ const stop = useChatStore((s) => s.stopGenerateMessage);
13
20
  const [sendMessage, updateInputMessage] = useChatStore((s) => [
14
21
  s.sendThreadMessage,
15
22
  s.updateThreadInputMessage,
16
23
  ]);
24
+ const checkGeminiChineseWarning = useGeminiChineseWarning();
17
25
 
18
- const isSendButtonDisabledByMessage = useChatStore(threadSelectors.isSendButtonDisabledByMessage);
19
-
20
- const canSend = !isSendButtonDisabledByMessage;
21
-
22
- const send = useCallback((params: UseSendMessageParams = {}) => {
26
+ const handleSend = async (params: UseSendMessageParams = {}) => {
23
27
  const store = useChatStore.getState();
28
+
24
29
  if (threadSelectors.isThreadAIGenerating(store)) return;
30
+ const canNotSend = threadSelectors.isSendButtonDisabledByMessage(store);
31
+
32
+ if (canNotSend) return;
33
+
34
+ const threadInputEditor = store.threadInputEditor;
25
35
 
26
- const isSendButtonDisabledByMessage = threadSelectors.isSendButtonDisabledByMessage(
27
- useChatStore.getState(),
28
- );
36
+ if (!threadInputEditor) {
37
+ console.warn('not found threadInputEditor instance');
38
+ return;
39
+ }
29
40
 
30
- const canSend = !isSendButtonDisabledByMessage;
31
- if (!canSend) return;
41
+ const inputMessage = threadInputEditor.getMarkdownContent();
32
42
 
33
43
  // if there is no message and no image, then we should not send the message
34
- if (!store.threadInputMessage) return;
44
+ if (!inputMessage) return;
35
45
 
36
- sendMessage({ message: store.threadInputMessage, ...params });
46
+ // Check for Chinese text warning with Gemini model
47
+ const agentStore = getAgentStoreState();
48
+ const currentModel = agentSelectors.currentAgentModel(agentStore);
49
+ const shouldContinue = await checkGeminiChineseWarning({
50
+ model: currentModel,
51
+ prompt: inputMessage,
52
+ scenario: 'chat',
53
+ });
37
54
 
38
- updateInputMessage('');
55
+ if (!shouldContinue) return;
56
+
57
+ updateInputMessage(inputMessage);
39
58
 
40
- // const hasSystemRole = agentSelectors.hasSystemRole(useAgentStore.getState());
41
- // const agentSetting = useAgentStore.getState().agentSettingInstance;
59
+ sendMessage({ message: inputMessage, ...params });
60
+
61
+ updateInputMessage('');
62
+ threadInputEditor.clearContent();
63
+ threadInputEditor.focus();
64
+ };
42
65
 
43
- // // if there is a system role, then we need to use agent setting instance to autocomplete agent meta
44
- // if (hasSystemRole && !!agentSetting) {
45
- // agentSetting.autocompleteAllMeta();
46
- // }
47
- }, []);
66
+ const send = async (params: UseSendMessageParams = {}) => {
67
+ setLoading(true);
68
+ await handleSend(params);
69
+ setLoading(false);
70
+ };
48
71
 
49
- return useMemo(() => ({ canSend, send }), [canSend]);
72
+ return useMemo(
73
+ () => ({ disabled: canNotSend, generating, loading, send, stop }),
74
+ [canNotSend, send, generating, stop, loading],
75
+ );
50
76
  };
@@ -11,7 +11,7 @@ interface ConversationProps {
11
11
  }
12
12
 
13
13
  const Conversation = memo<ConversationProps>(({ mobile }) => (
14
- <Flexbox height={'100%'}>
14
+ <>
15
15
  <Suspense
16
16
  fallback={
17
17
  <Flexbox flex={1} height={'100%'}>
@@ -22,7 +22,7 @@ const Conversation = memo<ConversationProps>(({ mobile }) => (
22
22
  <ChatList mobile={mobile} />
23
23
  </Suspense>
24
24
  <ChatInput />
25
- </Flexbox>
25
+ </>
26
26
  ));
27
27
 
28
28
  export default Conversation;
@@ -40,7 +40,7 @@ const Header = memo(() => {
40
40
  paddingBlock={6}
41
41
  paddingInline={8}
42
42
  style={{
43
- background: `linear-gradient(to bottom, ${theme.colorBgContainerSecondary}, ${theme.colorFillQuaternary})`,
43
+ borderBottom: `1px solid ${theme.colorBorderSecondary}`,
44
44
  }}
45
45
  title={<Title />}
46
46
  />
@@ -2,8 +2,8 @@ import isEqual from 'fast-deep-equal';
2
2
  import { useEffect } from 'react';
3
3
  import { useHotkeysContext } from 'react-hotkeys-hook';
4
4
 
5
+ import { useSend } from '@/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/useSend';
5
6
  import { useClearCurrentMessages } from '@/features/ChatInput/ActionBar/Clear';
6
- import { useSendMessage } from '@/features/ChatInput/useSend';
7
7
  import { useOpenChatSettings } from '@/hooks/useInterceptingRoutes';
8
8
  import { useActionSWR } from '@/libs/swr';
9
9
  import { useChatStore } from '@/store/chat';
@@ -75,8 +75,10 @@ export const useToggleRightPanelHotkey = () => {
75
75
  };
76
76
 
77
77
  export const useAddUserMessageHotkey = () => {
78
- const { send } = useSendMessage();
79
- return useHotkeyById(HotkeyEnum.AddUserMessage, () => send({ onlyAddUserMessage: true }));
78
+ const { send } = useSend();
79
+ return useHotkeyById(HotkeyEnum.AddUserMessage, () => {
80
+ send({ onlyAddUserMessage: true });
81
+ });
80
82
  };
81
83
 
82
84
  export const useClearCurrentMessagesHotkey = () => {