@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,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;
@@ -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';