@lobehub/chat 1.123.4 → 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 (126) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/changelog/v1.json +9 -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/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/{Footer/MessageFromUrl.tsx → MessageFromUrl.tsx} +3 -2
  49. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/index.tsx +129 -28
  50. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/index.tsx +44 -66
  51. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/useSend.ts +141 -0
  52. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/Content.tsx +7 -1
  53. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/QuestionSuggest.tsx +3 -2
  54. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/OpeningQuestions.tsx +3 -2
  55. package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/HeaderAction.tsx +18 -2
  56. package/src/features/ChatInput/ActionBar/STT/common.tsx +41 -47
  57. package/src/features/ChatInput/{Topic → ActionBar/SaveTopic}/index.tsx +15 -4
  58. package/src/features/ChatInput/ActionBar/Typo/index.tsx +22 -0
  59. package/src/features/ChatInput/ActionBar/components/Action.tsx +4 -0
  60. package/src/features/ChatInput/ActionBar/config.ts +7 -1
  61. package/src/features/ChatInput/ActionBar/index.tsx +40 -51
  62. package/src/features/ChatInput/ChatInputProvider.tsx +54 -0
  63. package/src/features/ChatInput/Desktop/FilePreview/FileItem/index.tsx +20 -11
  64. package/src/features/ChatInput/Desktop/FilePreview/FileList.tsx +16 -15
  65. package/src/features/ChatInput/Desktop/index.tsx +81 -68
  66. package/src/features/ChatInput/InputEditor/index.tsx +134 -0
  67. package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/File.tsx +1 -2
  68. package/src/features/ChatInput/Mobile/FilePreview/index.tsx +44 -0
  69. package/src/features/ChatInput/Mobile/index.tsx +72 -0
  70. package/src/features/ChatInput/SendArea/ExpandButton.tsx +30 -0
  71. package/src/features/ChatInput/SendArea/SendButton.tsx +29 -0
  72. package/src/features/ChatInput/SendArea/ShortcutHint.tsx +52 -0
  73. package/src/features/ChatInput/SendArea/index.tsx +36 -0
  74. package/src/features/ChatInput/StoreUpdater.tsx +41 -0
  75. package/src/features/ChatInput/TypoBar/index.tsx +139 -0
  76. package/src/features/ChatInput/hooks/useChatInputEditor.ts +36 -0
  77. package/src/features/ChatInput/index.ts +7 -0
  78. package/src/features/ChatInput/store/action.ts +75 -0
  79. package/src/features/ChatInput/store/index.ts +23 -0
  80. package/src/features/ChatInput/store/initialState.ts +54 -0
  81. package/src/features/ChatInput/store/selectors.ts +5 -0
  82. package/src/features/Conversation/components/BackBottom/style.ts +1 -1
  83. package/src/features/Conversation/components/SkeletonList.tsx +10 -3
  84. package/src/features/Conversation/components/VirtualizedList/index.tsx +53 -44
  85. package/src/features/Conversation/components/WideScreenContainer/index.tsx +43 -0
  86. package/src/features/Portal/Thread/Chat/ChatInput/index.tsx +49 -42
  87. package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +48 -22
  88. package/src/features/Portal/Thread/Chat/index.tsx +2 -2
  89. package/src/features/Portal/Thread/Header/index.tsx +1 -1
  90. package/src/hooks/useHotkeys/chatScope.ts +5 -3
  91. package/src/layout/GlobalProvider/Editor.tsx +27 -0
  92. package/src/layout/GlobalProvider/Locale.tsx +3 -23
  93. package/src/locales/default/chat.ts +7 -2
  94. package/src/locales/default/editor.ts +47 -0
  95. package/src/locales/default/index.ts +2 -0
  96. package/src/services/aiChat.ts +8 -2
  97. package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +42 -33
  98. package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +174 -35
  99. package/src/store/chat/slices/aiChat/initialState.ts +19 -0
  100. package/src/store/chat/slices/aiChat/selectors.ts +18 -0
  101. package/src/store/global/action.test.ts +6 -5
  102. package/src/store/global/actions/__tests__/general.test.ts +6 -6
  103. package/src/store/global/actions/workspacePane.ts +6 -0
  104. package/src/store/global/initialState.ts +2 -4
  105. package/src/store/global/selectors/systemStatus.test.ts +1 -2
  106. package/src/store/global/selectors/systemStatus.ts +2 -5
  107. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/SendMore.tsx +0 -104
  108. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/ShortcutHint.tsx +0 -40
  109. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +0 -125
  110. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.test.tsx +0 -332
  111. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.tsx +0 -29
  112. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files/index.tsx +0 -33
  113. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/Container.tsx +0 -41
  114. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/index.tsx +0 -156
  115. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Send.tsx +0 -33
  116. package/src/features/ChatInput/Desktop/Header/index.tsx +0 -30
  117. package/src/features/ChatInput/Desktop/InputArea/index.tsx +0 -143
  118. package/src/features/ChatInput/Desktop/__tests__/useAutoFocus.test.ts +0 -45
  119. package/src/features/ChatInput/Desktop/useAutoFocus.ts +0 -13
  120. package/src/features/ChatInput/useSend.ts +0 -102
  121. package/src/features/Portal/Thread/Chat/ChatInput/Footer.tsx +0 -90
  122. package/src/features/Portal/Thread/Chat/ChatInput/TextArea.tsx +0 -30
  123. package/src/libs/trpc/client/types.ts +0 -18
  124. /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/Image.tsx +0 -0
  125. /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/index.tsx +0 -0
  126. /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/style.ts +0 -0
@@ -1,82 +1,95 @@
1
1
  'use client';
2
2
 
3
- import { DraggablePanel } from '@lobehub/ui';
4
- import { ReactNode, memo, useCallback, useState } from 'react';
3
+ import { ChatInput, ChatInputActionBar } from '@lobehub/editor/react';
4
+ import { createStyles } from 'antd-style';
5
+ import { memo, useEffect } from 'react';
5
6
  import { Flexbox } from 'react-layout-kit';
6
7
 
7
- import { CHAT_TEXTAREA_HEIGHT, CHAT_TEXTAREA_MAX_HEIGHT } from '@/const/layoutTokens';
8
+ import { useChatInputStore } from '@/features/ChatInput/store';
9
+ import { useChatStore } from '@/store/chat';
10
+ import { chatSelectors } from '@/store/chat/selectors';
8
11
 
9
- import { ActionKeys } from '../ActionBar/config';
10
- import LocalFiles from './FilePreview';
11
- import Head from './Header';
12
+ import ActionBar from '../ActionBar';
13
+ import InputEditor from '../InputEditor';
14
+ import SendArea from '../SendArea';
15
+ import ShortcutHint from '../SendArea/ShortcutHint';
16
+ import TypoBar from '../TypoBar';
17
+ import FilePreview from './FilePreview';
12
18
 
13
- export type FooterRender = (params: {
14
- expand: boolean;
15
- onExpandChange: (expand: boolean) => void;
16
- }) => ReactNode;
19
+ const useStyles = createStyles(({ css, token }) => ({
20
+ container: css`
21
+ .show-on-hover {
22
+ opacity: 0;
23
+ }
17
24
 
18
- interface DesktopChatInputProps {
19
- inputHeight: number;
20
- leftActions: ActionKeys[];
21
- onInputHeightChange?: (height: number) => void;
22
- renderFooter: FooterRender;
23
- renderTextArea: (onSend: () => void) => ReactNode;
24
- rightActions: ActionKeys[];
25
- }
25
+ &:hover {
26
+ .show-on-hover {
27
+ opacity: 1;
28
+ }
29
+ }
30
+ `,
31
+ fullscreen: css`
32
+ position: absolute;
33
+ z-index: 100;
34
+ inset: 0;
26
35
 
27
- const DesktopChatInput = memo<DesktopChatInputProps>(
28
- ({
29
- leftActions,
30
- rightActions,
31
- renderTextArea,
32
- inputHeight,
33
- onInputHeightChange,
34
- renderFooter,
35
- }) => {
36
- const [expand, setExpand] = useState<boolean>(false);
37
- const onSend = useCallback(() => {
38
- setExpand(false);
39
- }, []);
36
+ width: 100%;
37
+ height: 100%;
38
+ padding: 12px;
40
39
 
41
- return (
42
- <>
43
- {!expand && leftActions.includes('fileUpload') && <LocalFiles />}
44
- <DraggablePanel
45
- fullscreen={expand}
46
- maxHeight={CHAT_TEXTAREA_MAX_HEIGHT}
47
- minHeight={CHAT_TEXTAREA_HEIGHT}
48
- onSizeChange={(_, size) => {
49
- if (!size) return;
50
- const height =
51
- typeof size.height === 'string' ? Number.parseInt(size.height) : size.height;
52
- if (!height) return;
40
+ background: ${token.colorBgContainerSecondary};
41
+ `,
42
+ }));
53
43
 
54
- onInputHeightChange?.(height);
55
- }}
56
- placement="bottom"
57
- size={{ height: inputHeight, width: '100%' }}
58
- style={{ zIndex: 10 }}
59
- >
60
- <Flexbox
61
- gap={8}
62
- height={'100%'}
63
- paddingBlock={'4px 16px'}
64
- style={{ minHeight: CHAT_TEXTAREA_HEIGHT, position: 'relative' }}
65
- >
66
- <Head
67
- expand={expand}
68
- leftActions={leftActions}
69
- rightActions={rightActions}
70
- setExpand={setExpand}
44
+ const DesktopChatInput = memo<{ showFootnote?: boolean }>(({ showFootnote }) => {
45
+ const [slashMenuRef, expand, showTypoBar, editor, leftActions] = useChatInputStore((s) => [
46
+ s.slashMenuRef,
47
+ s.expand,
48
+ s.showTypoBar,
49
+ s.editor,
50
+ s.leftActions,
51
+ ]);
52
+
53
+ const { styles, cx } = useStyles();
54
+
55
+ const chatKey = useChatStore(chatSelectors.currentChatKey);
56
+
57
+ useEffect(() => {
58
+ if (editor) editor.focus();
59
+ }, [chatKey, editor]);
60
+
61
+ const fileNode = leftActions.flat().includes('fileUpload') && <FilePreview />;
62
+
63
+ return (
64
+ <>
65
+ {!expand && fileNode}
66
+ <Flexbox
67
+ className={cx(styles.container, expand && styles.fullscreen)}
68
+ paddingBlock={showFootnote ? 0 : '0 12px'}
69
+ paddingInline={12}
70
+ >
71
+ <ChatInput
72
+ footer={
73
+ <ChatInputActionBar
74
+ left={<ActionBar />}
75
+ right={<SendArea />}
76
+ style={{
77
+ paddingRight: 8,
78
+ }}
71
79
  />
72
- {renderTextArea(onSend)}
73
- {renderFooter({ expand, onExpandChange: setExpand })}
74
- </Flexbox>
75
- </DraggablePanel>
76
- </>
77
- );
78
- },
79
- );
80
+ }
81
+ fullscreen={expand}
82
+ header={showTypoBar && <TypoBar />}
83
+ slashMenuRef={slashMenuRef}
84
+ >
85
+ {expand && fileNode}
86
+ <InputEditor />
87
+ </ChatInput>
88
+ {showFootnote && !expand && <ShortcutHint />}
89
+ </Flexbox>
90
+ </>
91
+ );
92
+ });
80
93
 
81
94
  DesktopChatInput.displayName = 'DesktopChatInput';
82
95
 
@@ -0,0 +1,134 @@
1
+ import { isDesktop } from '@lobechat/const';
2
+ import { HotkeyEnum } from '@lobechat/types';
3
+ import { isCommandPressed } from '@lobechat/utils';
4
+ import {
5
+ INSERT_TABLE_COMMAND,
6
+ ReactCodeblockPlugin,
7
+ ReactHRPlugin,
8
+ ReactLinkPlugin,
9
+ ReactListPlugin,
10
+ ReactTablePlugin,
11
+ } from '@lobehub/editor';
12
+ import { Editor, SlashMenu, useEditorState } from '@lobehub/editor/react';
13
+ import { Table2Icon } from 'lucide-react';
14
+ import { memo, useEffect, useRef } from 'react';
15
+ import { useHotkeysContext } from 'react-hotkeys-hook';
16
+ import { useTranslation } from 'react-i18next';
17
+
18
+ import { useUserStore } from '@/store/user';
19
+ import { preferenceSelectors } from '@/store/user/selectors';
20
+
21
+ import { useChatInputStore, useStoreApi } from '../store';
22
+
23
+ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
24
+ const [editor, slashMenuRef, send, updateMarkdownContent] = useChatInputStore((s) => [
25
+ s.editor,
26
+ s.slashMenuRef,
27
+ s.handleSendButton,
28
+ s.updateMarkdownContent,
29
+ ]);
30
+
31
+ const storeApi = useStoreApi();
32
+
33
+ const state = useEditorState(editor);
34
+ const { enableScope, disableScope } = useHotkeysContext();
35
+ const { t } = useTranslation(['editor', 'chat']);
36
+
37
+ const isChineseInput = useRef(false);
38
+
39
+ const useCmdEnterToSend = useUserStore(preferenceSelectors.useCmdEnterToSend);
40
+
41
+ useEffect(() => {
42
+ const fn = (e: BeforeUnloadEvent) => {
43
+ if (!state.isEmpty) {
44
+ // set returnValue to trigger alert modal
45
+ // Note: No matter what value is set, the browser will display the standard text
46
+ e.returnValue = 'You are typing something, are you sure you want to leave?';
47
+ }
48
+ };
49
+ window.addEventListener('beforeunload', fn);
50
+ return () => {
51
+ window.removeEventListener('beforeunload', fn);
52
+ };
53
+ }, [state.isEmpty]);
54
+
55
+ return (
56
+ <Editor
57
+ autoFocus
58
+ content={''}
59
+ editor={editor}
60
+ onBlur={() => {
61
+ disableScope(HotkeyEnum.AddUserMessage);
62
+ }}
63
+ onChange={() => {
64
+ updateMarkdownContent();
65
+ }}
66
+ onCompositionEnd={() => {
67
+ isChineseInput.current = false;
68
+ }}
69
+ onCompositionStart={() => {
70
+ isChineseInput.current = true;
71
+ }}
72
+ onContextMenu={async ({ event: e, editor }) => {
73
+ if (isDesktop) {
74
+ e.preventDefault();
75
+ const { electronSystemService } = await import('@/services/electron/system');
76
+
77
+ const selectionValue = editor.getSelectionDocument('markdown') as unknown as string;
78
+ const hasSelection = !!selectionValue;
79
+
80
+ await electronSystemService.showContextMenu('editor', {
81
+ hasSelection,
82
+ value: selectionValue,
83
+ });
84
+ }
85
+ }}
86
+ onFocus={() => {
87
+ enableScope(HotkeyEnum.AddUserMessage);
88
+ }}
89
+ onInit={(editor) => storeApi.setState({ editor })}
90
+ onPressEnter={({ event: e }) => {
91
+ if (e.altKey || e.shiftKey || isChineseInput.current) return;
92
+ const commandKey = isCommandPressed(e);
93
+ // when user like cmd + enter to send message
94
+ if (useCmdEnterToSend) {
95
+ if (commandKey) send();
96
+ } else {
97
+ if (!commandKey) send();
98
+ }
99
+ }}
100
+ placeholder={t('sendPlaceholder', { ns: 'chat' })}
101
+ plugins={[
102
+ ReactListPlugin,
103
+ ReactLinkPlugin,
104
+ ReactCodeblockPlugin,
105
+ ReactHRPlugin,
106
+ ReactTablePlugin,
107
+ ]}
108
+ slashOption={{
109
+ items: [
110
+ {
111
+ icon: Table2Icon,
112
+ key: 'table',
113
+ label: t('typobar.table'),
114
+ onSelect: (editor) => {
115
+ editor.dispatchCommand(INSERT_TABLE_COMMAND, { columns: '3', rows: '3' });
116
+ },
117
+ },
118
+ ],
119
+ renderComp: (props) => {
120
+ return <SlashMenu {...props} getPopupContainer={() => (slashMenuRef as any)?.current} />;
121
+ },
122
+ }}
123
+ style={{
124
+ minHeight: defaultRows > 1 ? defaultRows * 23 : undefined,
125
+ }}
126
+ type={'text'}
127
+ variant={'chat'}
128
+ />
129
+ );
130
+ });
131
+
132
+ InputEditor.displayName = 'InputEditor';
133
+
134
+ export default InputEditor;
@@ -5,10 +5,9 @@ import { memo } from 'react';
5
5
  import { Flexbox } from 'react-layout-kit';
6
6
 
7
7
  import FileIcon from '@/components/FileIcon';
8
+ import UploadDetail from '@/features/ChatInput/components/UploadDetail';
8
9
  import { UploadFileItem } from '@/types/files';
9
10
 
10
- import UploadDetail from '../../../../../../../../../../../features/ChatInput/components/UploadDetail';
11
-
12
11
  const useStyles = createStyles(({ css, token }) => ({
13
12
  container: css`
14
13
  cursor: pointer;
@@ -0,0 +1,44 @@
1
+ import { PreviewGroup, ScrollShadow } from '@lobehub/ui';
2
+ import { createStyles } from 'antd-style';
3
+ import isEqual from 'fast-deep-equal';
4
+ import { memo } from 'react';
5
+ import { Flexbox } from 'react-layout-kit';
6
+
7
+ import { useChatInputStore } from '@/features/ChatInput/store';
8
+ import { filesSelectors, useFileStore } from '@/store/file';
9
+
10
+ import FileItem from './FileItem';
11
+
12
+ const useStyles = createStyles(({ css }) => ({
13
+ container: css`
14
+ overflow-x: scroll;
15
+ width: 100%;
16
+ `,
17
+ }));
18
+
19
+ const FilePreview = memo(() => {
20
+ const expand = useChatInputStore((s) => s.expand);
21
+ const list = useFileStore(filesSelectors.chatUploadFileList, isEqual);
22
+ const { styles } = useStyles();
23
+ if (!list || list?.length === 0) return null;
24
+
25
+ return (
26
+ <ScrollShadow
27
+ className={styles.container}
28
+ hideScrollBar
29
+ horizontal
30
+ orientation={'horizontal'}
31
+ size={8}
32
+ >
33
+ <Flexbox gap={6} horizontal paddingBlock={8} paddingInline={expand ? 0 : 12}>
34
+ <PreviewGroup>
35
+ {list.map((i) => (
36
+ <FileItem {...i} key={i.id} loading={i.status === 'pending'} />
37
+ ))}
38
+ </PreviewGroup>
39
+ </Flexbox>
40
+ </ScrollShadow>
41
+ );
42
+ });
43
+
44
+ export default FilePreview;
@@ -0,0 +1,72 @@
1
+ 'use client';
2
+
3
+ import { ChatInput, ChatInputActionBar } from '@lobehub/editor/react';
4
+ import { createStyles } from 'antd-style';
5
+ import dynamic from 'next/dynamic';
6
+ import { memo } from 'react';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { useChatInputStore } from '@/features/ChatInput/store';
10
+
11
+ import ActionBar from '../ActionBar';
12
+ import InputEditor from '../InputEditor';
13
+ import SendArea from '../SendArea';
14
+
15
+ const FilePreview = dynamic(() => import('./FilePreview'), { ssr: false });
16
+
17
+ const useStyles = createStyles(({ css, token }) => ({
18
+ container: css``,
19
+ fullscreen: css`
20
+ position: absolute;
21
+ z-index: 100;
22
+ inset: 0;
23
+
24
+ width: 100%;
25
+ height: 100%;
26
+ padding: 12px;
27
+
28
+ background: ${token.colorBgLayout};
29
+ `,
30
+ }));
31
+
32
+ const DesktopChatInput = memo(() => {
33
+ const [slashMenuRef, expand] = useChatInputStore((s) => [s.slashMenuRef, s.expand]);
34
+ const leftActions = useChatInputStore((s) => s.leftActions);
35
+
36
+ const { styles, cx } = useStyles();
37
+
38
+ const fileNode = leftActions.flat().includes('fileUpload') && <FilePreview />;
39
+
40
+ return (
41
+ <>
42
+ {!expand && fileNode}
43
+ <Flexbox
44
+ className={cx(styles.container, expand && styles.fullscreen)}
45
+ paddingBlock={'0 12px'}
46
+ paddingInline={12}
47
+ >
48
+ <ChatInput
49
+ footer={
50
+ <ChatInputActionBar
51
+ left={<div />}
52
+ right={<SendArea />}
53
+ style={{
54
+ paddingRight: 8,
55
+ }}
56
+ />
57
+ }
58
+ fullscreen={expand}
59
+ header={<ChatInputActionBar left={<ActionBar />} />}
60
+ slashMenuRef={slashMenuRef}
61
+ >
62
+ {expand && fileNode}
63
+ <InputEditor defaultRows={1} />
64
+ </ChatInput>
65
+ </Flexbox>
66
+ </>
67
+ );
68
+ });
69
+
70
+ DesktopChatInput.displayName = 'DesktopChatInput';
71
+
72
+ export default DesktopChatInput;
@@ -0,0 +1,30 @@
1
+ import { ActionIcon } from '@lobehub/ui';
2
+ import { Maximize2Icon, Minimize2Icon } from 'lucide-react';
3
+ import { memo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ import { useChatInputStore } from '@/features/ChatInput/store';
7
+
8
+ const ExpandButton = memo(() => {
9
+ const { t } = useTranslation('editor');
10
+ const [expand, setExpand, editor] = useChatInputStore((s) => [s.expand, s.setExpand, s.editor]);
11
+ return (
12
+ <ActionIcon
13
+ className="show-on-hover"
14
+ icon={expand ? Minimize2Icon : Maximize2Icon}
15
+ onClick={() => {
16
+ setExpand?.(!expand);
17
+ editor?.focus();
18
+ }}
19
+ size={{ blockSize: 32, size: 16, strokeWidth: 2.3 }}
20
+ style={{
21
+ zIndex: 10,
22
+ }}
23
+ title={t(expand ? 'actions.expand.off' : 'actions.expand.on')}
24
+ />
25
+ );
26
+ });
27
+
28
+ ExpandButton.displayName = 'ExpandButton';
29
+
30
+ export default ExpandButton;
@@ -0,0 +1,29 @@
1
+ import { SendButton as Send } from '@lobehub/editor/react';
2
+ import isEqual from 'fast-deep-equal';
3
+ import { memo } from 'react';
4
+
5
+ import { selectors, useChatInputStore } from '../store';
6
+
7
+ const SendButton = memo(() => {
8
+ const sendMenu = useChatInputStore((s) => s.sendMenu);
9
+ const shape = useChatInputStore((s) => s.sendButtonProps?.shape);
10
+ const { generating, disabled } = useChatInputStore(selectors.sendButtonProps, isEqual);
11
+ const [send, handleStop] = useChatInputStore((s) => [s.handleSendButton, s.handleStop]);
12
+
13
+ return (
14
+ <Send
15
+ disabled={disabled}
16
+ generating={generating}
17
+ menu={sendMenu as any}
18
+ onClick={() => send()}
19
+ onStop={() => handleStop()}
20
+ placement={'topRight'}
21
+ shape={shape}
22
+ trigger={['hover']}
23
+ />
24
+ );
25
+ });
26
+
27
+ SendButton.displayName = 'SendButton';
28
+
29
+ export default SendButton;
@@ -0,0 +1,52 @@
1
+ import { Hotkey, Text, combineKeys } from '@lobehub/ui';
2
+ import { useTheme } from 'antd-style';
3
+ import { memo } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Flexbox } from 'react-layout-kit';
6
+
7
+ import { useUserStore } from '@/store/user';
8
+ import { preferenceSelectors } from '@/store/user/selectors';
9
+ import { KeyEnum } from '@/types/hotkey';
10
+
11
+ const ShortcutHint = memo(() => {
12
+ const { t } = useTranslation('chat');
13
+ const theme = useTheme();
14
+
15
+ const useCmdEnterToSend = useUserStore(preferenceSelectors.useCmdEnterToSend);
16
+
17
+ const sendShortcut = useCmdEnterToSend
18
+ ? combineKeys([KeyEnum.Mod, KeyEnum.Enter])
19
+ : KeyEnum.Enter;
20
+
21
+ const wrapperShortcut = useCmdEnterToSend
22
+ ? KeyEnum.Enter
23
+ : combineKeys([KeyEnum.Mod, KeyEnum.Enter]);
24
+
25
+ return (
26
+ <Text fontSize={12} style={{ color: theme.colorTextQuaternary, userSelect: 'none', zIndex: 1 }}>
27
+ <Flexbox align={'center'} gap={4} horizontal justify={'flex-end'} paddingBlock={4}>
28
+ <Hotkey
29
+ keys={sendShortcut}
30
+ style={{ color: 'inherit' }}
31
+ styles={{
32
+ kbdStyle: { color: 'inherit' },
33
+ }}
34
+ variant={'borderless'}
35
+ />
36
+ <span>{t('input.send')}</span>
37
+ <span>/</span>
38
+ <Hotkey
39
+ keys={wrapperShortcut}
40
+ style={{ color: 'inherit' }}
41
+ styles={{
42
+ kbdStyle: { color: 'inherit' },
43
+ }}
44
+ variant={'borderless'}
45
+ />
46
+ <span>{t('input.warp')}</span>
47
+ </Flexbox>
48
+ </Text>
49
+ );
50
+ });
51
+
52
+ export default ShortcutHint;
@@ -0,0 +1,36 @@
1
+ import isEqual from 'fast-deep-equal';
2
+ import { memo, useMemo } from 'react';
3
+ import { Flexbox } from 'react-layout-kit';
4
+
5
+ import { ActionKey, actionMap } from '../ActionBar/config';
6
+ import { useChatInputStore } from '../store';
7
+ import ExpandButton from './ExpandButton';
8
+ import SendButton from './SendButton';
9
+
10
+ const mapActionsToItems = (keys: ActionKey[]) =>
11
+ keys.map((actionKey) => {
12
+ const Render = actionMap[actionKey];
13
+ return <Render key={actionKey} />;
14
+ });
15
+
16
+ const SendArea = memo(() => {
17
+ const allowExpand = useChatInputStore((s) => s.allowExpand);
18
+ const rightActions = useChatInputStore((s) => s.rightActions, isEqual);
19
+
20
+ const items = useMemo(
21
+ () => mapActionsToItems((rightActions as ActionKey[]) || []),
22
+ [rightActions],
23
+ );
24
+
25
+ return (
26
+ <Flexbox align={'center'} flex={'none'} gap={6} horizontal>
27
+ {allowExpand && <ExpandButton />}
28
+ {items}
29
+ <SendButton />
30
+ </Flexbox>
31
+ );
32
+ });
33
+
34
+ SendArea.displayName = 'SendArea';
35
+
36
+ export default SendArea;
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { ForwardedRef, memo, useImperativeHandle } from 'react';
4
+ import { createStoreUpdater } from 'zustand-utils';
5
+
6
+ import { ChatInputEditor, useChatInputEditor } from './hooks/useChatInputEditor';
7
+ import { PublicState, useStoreApi } from './store';
8
+
9
+ export interface StoreUpdaterProps extends Partial<PublicState> {
10
+ chatInputEditorRef?: ForwardedRef<ChatInputEditor | null>;
11
+ }
12
+
13
+ const StoreUpdater = memo<StoreUpdaterProps>(
14
+ ({
15
+ chatInputEditorRef,
16
+ mobile,
17
+ sendButtonProps,
18
+ leftActions,
19
+ rightActions,
20
+ onSend,
21
+ onMarkdownContentChange,
22
+ }) => {
23
+ const storeApi = useStoreApi();
24
+ const useStoreUpdater = createStoreUpdater(storeApi);
25
+ const editor = useChatInputEditor();
26
+
27
+ useStoreUpdater('mobile', mobile);
28
+ useStoreUpdater('leftActions', leftActions);
29
+ useStoreUpdater('rightActions', rightActions);
30
+
31
+ useStoreUpdater('sendButtonProps', sendButtonProps);
32
+ useStoreUpdater('onSend', onSend);
33
+ useStoreUpdater('onMarkdownContentChange', onMarkdownContentChange);
34
+
35
+ useImperativeHandle(chatInputEditorRef, () => editor);
36
+
37
+ return null;
38
+ },
39
+ );
40
+
41
+ export default StoreUpdater;