@lobehub/chat 1.124.3 → 1.125.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 (38) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/editor.json +7 -0
  4. package/locales/bg-BG/editor.json +7 -0
  5. package/locales/de-DE/editor.json +7 -0
  6. package/locales/en-US/editor.json +7 -0
  7. package/locales/es-ES/editor.json +7 -0
  8. package/locales/fa-IR/editor.json +7 -0
  9. package/locales/fr-FR/editor.json +7 -0
  10. package/locales/it-IT/editor.json +7 -0
  11. package/locales/ja-JP/editor.json +7 -0
  12. package/locales/ko-KR/editor.json +7 -0
  13. package/locales/nl-NL/editor.json +7 -0
  14. package/locales/pl-PL/editor.json +7 -0
  15. package/locales/pt-BR/editor.json +7 -0
  16. package/locales/ru-RU/editor.json +7 -0
  17. package/locales/tr-TR/editor.json +7 -0
  18. package/locales/vi-VN/editor.json +7 -0
  19. package/locales/zh-CN/editor.json +7 -0
  20. package/locales/zh-TW/editor.json +7 -0
  21. package/package.json +2 -2
  22. package/packages/model-bank/src/aiModels/qwen.ts +4 -0
  23. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/ClassicChat.tsx +153 -0
  24. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/GroupChat.tsx +153 -0
  25. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/index.tsx +3 -145
  26. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/ActionBar.tsx +30 -0
  27. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/Files/index.tsx +32 -0
  28. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/InputArea/Container.tsx +41 -0
  29. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/InputArea/index.tsx +156 -0
  30. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/Send.tsx +33 -0
  31. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/index.tsx +89 -0
  32. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/useSend.ts +102 -0
  33. package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/index.tsx +1 -1
  34. package/src/app/[variants]/(main)/settings/_layout/Mobile/Header.tsx +4 -0
  35. package/src/features/ChatInput/ActionBar/SaveTopic/index.tsx +4 -1
  36. package/src/features/ChatInput/InputEditor/index.tsx +20 -5
  37. package/src/features/ChatInput/TypoBar/index.tsx +17 -0
  38. package/src/locales/default/editor.ts +7 -0
@@ -0,0 +1,156 @@
1
+ import { ActionIcon, TextArea } from '@lobehub/ui';
2
+ import { SafeArea } from '@lobehub/ui/mobile';
3
+ import { useSize } from 'ahooks';
4
+ import { createStyles } from 'antd-style';
5
+ import { TextAreaRef } from 'antd/es/input/TextArea';
6
+ import { ChevronDown, ChevronUp } from 'lucide-react';
7
+ import { rgba } from 'polished';
8
+ import { CSSProperties, ReactNode, forwardRef, useEffect, useRef, useState } from 'react';
9
+ import { useTranslation } from 'react-i18next';
10
+ import { Flexbox } from 'react-layout-kit';
11
+
12
+ import InnerContainer from './Container';
13
+
14
+ const useStyles = createStyles(({ css, token }) => {
15
+ return {
16
+ container: css`
17
+ flex: none;
18
+ padding-block: 12px 12px;
19
+ border-block-start: 1px solid ${rgba(token.colorBorder, 0.25)};
20
+ background: ${token.colorFillQuaternary};
21
+ `,
22
+ expand: css`
23
+ position: absolute;
24
+ height: 100%;
25
+ `,
26
+ expandButton: css`
27
+ position: absolute;
28
+ inset-inline-start: 14px;
29
+ `,
30
+ textarea: css`
31
+ flex: 1;
32
+ transition: none !important;
33
+ `,
34
+ };
35
+ });
36
+
37
+ export interface MobileChatInputAreaProps {
38
+ bottomAddons?: ReactNode;
39
+ className?: string;
40
+ expand?: boolean;
41
+ loading?: boolean;
42
+ onInput?: (value: string) => void;
43
+ onSend?: () => void;
44
+ safeArea?: boolean;
45
+ setExpand?: (expand: boolean) => void;
46
+ style?: CSSProperties;
47
+ textAreaLeftAddons?: ReactNode;
48
+ textAreaRightAddons?: ReactNode;
49
+ topAddons?: ReactNode;
50
+ value: string;
51
+ }
52
+
53
+ const MobileChatInputArea = forwardRef<TextAreaRef, MobileChatInputAreaProps>(
54
+ (
55
+ {
56
+ className,
57
+ style,
58
+ topAddons,
59
+ textAreaLeftAddons,
60
+ textAreaRightAddons,
61
+ bottomAddons,
62
+ expand = false,
63
+ setExpand,
64
+ onSend,
65
+ onInput,
66
+ loading,
67
+ value,
68
+ safeArea,
69
+ },
70
+ ref,
71
+ ) => {
72
+ const { t } = useTranslation('chat');
73
+ const isChineseInput = useRef(false);
74
+ const containerRef = useRef<HTMLDivElement>(null);
75
+ const { cx, styles } = useStyles();
76
+ const size = useSize(containerRef);
77
+ const [showFullscreen, setShowFullscreen] = useState<boolean>(false);
78
+ const [isFocused, setIsFocused] = useState<boolean>(false);
79
+
80
+ useEffect(() => {
81
+ if (!size?.height) return;
82
+ setShowFullscreen(size.height > 72);
83
+ }, [size]);
84
+
85
+ const showAddons = !expand && !isFocused;
86
+
87
+ return (
88
+ <Flexbox
89
+ className={cx(styles.container, expand && styles.expand, className)}
90
+ gap={12}
91
+ style={style}
92
+ >
93
+ {topAddons && <Flexbox style={showAddons ? {} : { display: 'none' }}>{topAddons}</Flexbox>}
94
+ <Flexbox
95
+ className={cx(expand && styles.expand)}
96
+ ref={containerRef}
97
+ style={{ position: 'relative' }}
98
+ >
99
+ {showFullscreen && (
100
+ <ActionIcon
101
+ active
102
+ className={styles.expandButton}
103
+ icon={expand ? ChevronDown : ChevronUp}
104
+ onClick={() => setExpand?.(!expand)}
105
+ size={{ blockSize: 24, borderRadius: '50%', size: 14 }}
106
+ style={expand ? { top: 6 } : {}}
107
+ />
108
+ )}
109
+ <InnerContainer
110
+ bottomAddons={bottomAddons}
111
+ expand={expand}
112
+ textAreaLeftAddons={textAreaLeftAddons}
113
+ textAreaRightAddons={textAreaRightAddons}
114
+ topAddons={topAddons}
115
+ >
116
+ <TextArea
117
+ autoSize={expand ? false : { maxRows: 6, minRows: 0 }}
118
+ className={styles.textarea}
119
+ onBlur={(e) => {
120
+ onInput?.(e.target.value);
121
+ setIsFocused(false);
122
+ }}
123
+ onChange={(e) => {
124
+ onInput?.(e.target.value);
125
+ }}
126
+ onCompositionEnd={() => {
127
+ isChineseInput.current = false;
128
+ }}
129
+ onCompositionStart={() => {
130
+ isChineseInput.current = true;
131
+ }}
132
+ onFocus={() => setIsFocused(true)}
133
+ onPressEnter={(e) => {
134
+ if (!loading && !isChineseInput.current && e.shiftKey) {
135
+ e.preventDefault();
136
+ onSend?.();
137
+ }
138
+ }}
139
+ placeholder={t('sendPlaceholder')}
140
+ ref={ref}
141
+ style={{ height: 36, paddingBlock: 6 }}
142
+ value={value}
143
+ variant={expand ? 'borderless' : 'filled'}
144
+ />
145
+ </InnerContainer>
146
+ </Flexbox>
147
+ {bottomAddons && (
148
+ <Flexbox style={showAddons ? {} : { display: 'none' }}>{bottomAddons}</Flexbox>
149
+ )}
150
+ {safeArea && !isFocused && <SafeArea position={'bottom'} />}
151
+ </Flexbox>
152
+ );
153
+ },
154
+ );
155
+
156
+ export default MobileChatInputArea;
@@ -0,0 +1,33 @@
1
+ import { ActionIcon, type ActionIconSize, Button } from '@lobehub/ui';
2
+ import { Loader2, SendHorizontal } from 'lucide-react';
3
+ import { memo } from 'react';
4
+
5
+ export interface MobileChatSendButtonProps {
6
+ disabled?: boolean;
7
+ loading?: boolean;
8
+ onSend?: () => void;
9
+ onStop?: () => void;
10
+ }
11
+
12
+ const MobileChatSendButton = memo<MobileChatSendButtonProps>(
13
+ ({ loading, onStop, onSend, disabled }) => {
14
+ const size: ActionIconSize = {
15
+ blockSize: 36,
16
+ size: 16,
17
+ };
18
+
19
+ return loading ? (
20
+ <ActionIcon active icon={Loader2} onClick={onStop} size={size} spin />
21
+ ) : (
22
+ <Button
23
+ disabled={disabled}
24
+ icon={SendHorizontal}
25
+ onClick={onSend}
26
+ style={{ flex: 'none' }}
27
+ type={'primary'}
28
+ />
29
+ );
30
+ },
31
+ );
32
+
33
+ export default MobileChatSendButton;
@@ -0,0 +1,89 @@
1
+ 'use client';
2
+
3
+ import { Skeleton } from 'antd';
4
+ import { useTheme } from 'antd-style';
5
+ import { TextAreaRef } from 'antd/es/input/TextArea';
6
+ import { memo, useRef, useState } from 'react';
7
+ import { Flexbox } from 'react-layout-kit';
8
+
9
+ import { ActionKeys } from '@/features/ChatInput/ActionBar/config';
10
+ import { useInitAgentConfig } from '@/hooks/useInitAgentConfig';
11
+ import { useChatStore } from '@/store/chat';
12
+ import { chatSelectors } from '@/store/chat/selectors';
13
+
14
+ import ActionBar from './ActionBar';
15
+ import Files from './Files';
16
+ import InputArea from './InputArea';
17
+ import SendButton from './Send';
18
+ import { useSendMessage } from './useSend';
19
+
20
+ const defaultLeftActions: ActionKeys[] = [
21
+ 'model',
22
+ 'search',
23
+ 'fileUpload',
24
+ 'knowledgeBase',
25
+ 'tools',
26
+ 'params',
27
+ 'mainToken',
28
+ ];
29
+
30
+ const defaultRightActions: ActionKeys[] = ['saveTopic', 'clear'];
31
+
32
+ const MobileChatInput = memo(() => {
33
+ const theme = useTheme();
34
+ const ref = useRef<TextAreaRef>(null);
35
+ const [expand, setExpand] = useState<boolean>(false);
36
+ const { send: sendMessage, canSend } = useSendMessage();
37
+ const { isLoading } = useInitAgentConfig();
38
+
39
+ const [loading, value, onInput, onStop] = useChatStore((s) => [
40
+ chatSelectors.isAIGenerating(s),
41
+ s.inputMessage,
42
+ s.updateInputMessage,
43
+ s.stopGenerateMessage,
44
+ ]);
45
+
46
+ return (
47
+ <InputArea
48
+ expand={expand}
49
+ onInput={onInput}
50
+ onSend={() => {
51
+ setExpand(false);
52
+
53
+ sendMessage();
54
+ }}
55
+ ref={ref}
56
+ setExpand={setExpand}
57
+ style={{
58
+ background: theme.colorBgLayout,
59
+ top: expand ? 0 : undefined,
60
+ width: '100%',
61
+ zIndex: 101,
62
+ }}
63
+ textAreaRightAddons={
64
+ <SendButton disabled={!canSend} loading={loading} onSend={sendMessage} onStop={onStop} />
65
+ }
66
+ topAddons={
67
+ isLoading ? (
68
+ <Flexbox paddingInline={8}>
69
+ <Skeleton.Button active block size={'small'} />
70
+ </Flexbox>
71
+ ) : (
72
+ <>
73
+ <Files />
74
+ <ActionBar
75
+ leftActions={defaultLeftActions}
76
+ padding={'0 8px'}
77
+ rightActions={defaultRightActions}
78
+ />
79
+ </>
80
+ )
81
+ }
82
+ value={value}
83
+ />
84
+ );
85
+ });
86
+
87
+ MobileChatInput.displayName = 'MobileChatInput';
88
+
89
+ export default MobileChatInput;
@@ -0,0 +1,102 @@
1
+ import { useAnalytics } from '@lobehub/analytics/react';
2
+ import { useCallback, useMemo } from 'react';
3
+
4
+ import { useGeminiChineseWarning } from '@/hooks/useGeminiChineseWarning';
5
+ import { getAgentStoreState } from '@/store/agent';
6
+ import { agentSelectors } from '@/store/agent/selectors';
7
+ import { useChatStore } from '@/store/chat';
8
+ import { chatSelectors, topicSelectors } from '@/store/chat/selectors';
9
+ import { fileChatSelectors, useFileStore } from '@/store/file';
10
+ import { getUserStoreState } from '@/store/user';
11
+ import { SendMessageParams } from '@/types/message';
12
+
13
+ export type UseSendMessageParams = Pick<
14
+ SendMessageParams,
15
+ 'onlyAddUserMessage' | 'isWelcomeQuestion'
16
+ >;
17
+
18
+ export const useSendMessage = () => {
19
+ const [sendMessage, updateInputMessage] = useChatStore((s) => [
20
+ s.sendMessage,
21
+ s.updateInputMessage,
22
+ ]);
23
+ const { analytics } = useAnalytics();
24
+ const checkGeminiChineseWarning = useGeminiChineseWarning();
25
+
26
+ const clearChatUploadFileList = useFileStore((s) => s.clearChatUploadFileList);
27
+
28
+ const isUploadingFiles = useFileStore(fileChatSelectors.isUploadingFiles);
29
+ const isSendButtonDisabledByMessage = useChatStore(chatSelectors.isSendButtonDisabledByMessage);
30
+
31
+ const canSend = !isUploadingFiles && !isSendButtonDisabledByMessage;
32
+
33
+ const send = useCallback(async (params: UseSendMessageParams = {}) => {
34
+ const store = useChatStore.getState();
35
+ if (chatSelectors.isAIGenerating(store)) return;
36
+
37
+ // if uploading file or send button is disabled by message, then we should not send the message
38
+ const isUploadingFiles = fileChatSelectors.isUploadingFiles(useFileStore.getState());
39
+ const isSendButtonDisabledByMessage = chatSelectors.isSendButtonDisabledByMessage(
40
+ useChatStore.getState(),
41
+ );
42
+
43
+ const canSend = !isUploadingFiles && !isSendButtonDisabledByMessage;
44
+ if (!canSend) return;
45
+
46
+ const fileList = fileChatSelectors.chatUploadFileList(useFileStore.getState());
47
+ // if there is no message and no image, then we should not send the message
48
+ if (!store.inputMessage && fileList.length === 0) return;
49
+
50
+ // Check for Chinese text warning with Gemini model
51
+ const agentStore = getAgentStoreState();
52
+ const currentModel = agentSelectors.currentAgentModel(agentStore);
53
+ const shouldContinue = await checkGeminiChineseWarning({
54
+ model: currentModel,
55
+ prompt: store.inputMessage,
56
+ scenario: 'chat',
57
+ });
58
+
59
+ if (!shouldContinue) return;
60
+
61
+ sendMessage({
62
+ files: fileList,
63
+ message: store.inputMessage,
64
+ ...params,
65
+ });
66
+
67
+ updateInputMessage('');
68
+ clearChatUploadFileList();
69
+
70
+ // 获取分析数据
71
+ const userStore = getUserStoreState();
72
+
73
+ // 直接使用现有数据结构判断消息类型
74
+ const hasImages = fileList.some((file) => file.file?.type?.startsWith('image'));
75
+ const messageType = fileList.length === 0 ? 'text' : hasImages ? 'image' : 'file';
76
+
77
+ analytics?.track({
78
+ name: 'send_message',
79
+ properties: {
80
+ chat_id: store.activeId || 'unknown',
81
+ current_topic: topicSelectors.currentActiveTopic(store)?.title || null,
82
+ has_attachments: fileList.length > 0,
83
+ history_message_count: chatSelectors.activeBaseChats(store).length,
84
+ message: store.inputMessage,
85
+ message_length: store.inputMessage.length,
86
+ message_type: messageType,
87
+ selected_model: agentSelectors.currentAgentModel(agentStore),
88
+ session_id: store.activeId || 'inbox', // 当前活跃的会话ID
89
+ user_id: userStore.user?.id || 'anonymous',
90
+ },
91
+ });
92
+ // const hasSystemRole = agentSelectors.hasSystemRole(useAgentStore.getState());
93
+ // const agentSetting = useAgentStore.getState().agentSettingInstance;
94
+
95
+ // // if there is a system role, then we need to use agent setting instance to autocomplete agent meta
96
+ // if (hasSystemRole && !!agentSetting) {
97
+ // agentSetting.autocompleteAllMeta();
98
+ // }
99
+ }, []);
100
+
101
+ return useMemo(() => ({ canSend, send }), [canSend]);
102
+ };
@@ -1,5 +1,5 @@
1
1
  import DesktopChatInput from './Desktop';
2
- import MobileChatInput from './Mobile';
2
+ import MobileChatInput from './V1Mobile';
3
3
 
4
4
  const ChatInput = ({ mobile }: { mobile: boolean }) => {
5
5
  const Input = mobile ? MobileChatInput : DesktopChatInput;
@@ -26,10 +26,14 @@ const Header = memo(() => {
26
26
  const pathname = usePathname();
27
27
  const isProvider = pathname.includes('/settings/provider/');
28
28
  const providerName = useProviderName(activeSettingsKey);
29
+ const isProviderList = pathname === '/settings/provider';
30
+ const isProviderDetail = isProvider && !isProviderList;
29
31
 
30
32
  const handleBackClick = () => {
31
33
  if (isSessionActive && showMobileWorkspace) {
32
34
  router.push('/chat');
35
+ } else if (isProviderDetail) {
36
+ router.push('/settings/provider');
33
37
  } else {
34
38
  router.push(enableAuth ? '/me/settings' : '/me');
35
39
  }
@@ -5,13 +5,14 @@ import { memo, useState } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
  import { Flexbox } from 'react-layout-kit';
7
7
 
8
+ import { useIsMobile } from '@/hooks/useIsMobile';
8
9
  import { useActionSWR } from '@/libs/swr';
9
10
  import { useChatStore } from '@/store/chat';
10
11
  import { useUserStore } from '@/store/user';
11
12
  import { settingsSelectors } from '@/store/user/selectors';
12
13
  import { HotkeyEnum } from '@/types/hotkey';
13
14
 
14
- const SaveTopic = memo<{ mobile?: boolean }>(({ mobile }) => {
15
+ const SaveTopic = memo(() => {
15
16
  const { t } = useTranslation('chat');
16
17
  const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.SaveTopic));
17
18
  const [hasTopic, openNewTopicOrSaveTopic] = useChatStore((s) => [
@@ -19,6 +20,8 @@ const SaveTopic = memo<{ mobile?: boolean }>(({ mobile }) => {
19
20
  s.openNewTopicOrSaveTopic,
20
21
  ]);
21
22
 
23
+ const mobile = useIsMobile();
24
+
22
25
  const { mutate, isValidating } = useActionSWR('openNewTopicOrSaveTopic', openNewTopicOrSaveTopic);
23
26
 
24
27
  const [confirmOpened, setConfirmOpened] = useState(false);
@@ -3,13 +3,15 @@ import { HotkeyEnum } from '@lobechat/types';
3
3
  import { isCommandPressed } from '@lobechat/utils';
4
4
  import {
5
5
  INSERT_TABLE_COMMAND,
6
+ ReactCodePlugin,
6
7
  ReactCodeblockPlugin,
7
8
  ReactHRPlugin,
8
9
  ReactLinkPlugin,
9
10
  ReactListPlugin,
11
+ ReactMathPlugin,
10
12
  ReactTablePlugin,
11
13
  } from '@lobehub/editor';
12
- import { Editor, SlashMenu, useEditorState } from '@lobehub/editor/react';
14
+ import { Editor, FloatMenu, SlashMenu, useEditorState } from '@lobehub/editor/react';
13
15
  import { Table2Icon } from 'lucide-react';
14
16
  import { memo, useEffect, useRef } from 'react';
15
17
  import { useHotkeysContext } from 'react-hotkeys-hook';
@@ -21,11 +23,12 @@ import { preferenceSelectors } from '@/store/user/selectors';
21
23
  import { useChatInputStore, useStoreApi } from '../store';
22
24
 
23
25
  const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
24
- const [editor, slashMenuRef, send, updateMarkdownContent] = useChatInputStore((s) => [
26
+ const [editor, slashMenuRef, send, updateMarkdownContent, expand] = useChatInputStore((s) => [
25
27
  s.editor,
26
28
  s.slashMenuRef,
27
29
  s.handleSendButton,
28
30
  s.updateMarkdownContent,
31
+ s.expand,
29
32
  ]);
30
33
 
31
34
  const storeApi = useStoreApi();
@@ -101,9 +104,17 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
101
104
  plugins={[
102
105
  ReactListPlugin,
103
106
  ReactLinkPlugin,
107
+ ReactCodePlugin,
104
108
  ReactCodeblockPlugin,
105
109
  ReactHRPlugin,
106
110
  ReactTablePlugin,
111
+ Editor.withProps(ReactMathPlugin, {
112
+ renderComp: expand
113
+ ? undefined
114
+ : (props) => (
115
+ <FloatMenu {...props} getPopupContainer={() => (slashMenuRef as any)?.current} />
116
+ ),
117
+ }),
107
118
  ]}
108
119
  slashOption={{
109
120
  items: [
@@ -116,9 +127,13 @@ const InputEditor = memo<{ defaultRows?: number }>(({ defaultRows = 2 }) => {
116
127
  },
117
128
  },
118
129
  ],
119
- renderComp: (props) => {
120
- return <SlashMenu {...props} getPopupContainer={() => (slashMenuRef as any)?.current} />;
121
- },
130
+ renderComp: expand
131
+ ? undefined
132
+ : (props) => {
133
+ return (
134
+ <SlashMenu {...props} getPopupContainer={() => (slashMenuRef as any)?.current} />
135
+ );
136
+ },
122
137
  }}
123
138
  style={{
124
139
  minHeight: defaultRows > 1 ? defaultRows * 23 : undefined,
@@ -13,7 +13,9 @@ import {
13
13
  LinkIcon,
14
14
  ListIcon,
15
15
  ListOrderedIcon,
16
+ ListTodoIcon,
16
17
  MessageSquareQuote,
18
+ SigmaIcon,
17
19
  SquareDashedBottomCodeIcon,
18
20
  StrikethroughIcon,
19
21
  } from 'lucide-react';
@@ -79,6 +81,15 @@ const TypoBar = memo(() => {
79
81
  label: t('typobar.numberList'),
80
82
  onClick: editorState.numberList,
81
83
  },
84
+ {
85
+ icon: ListTodoIcon,
86
+ key: 'tasklist',
87
+ label: t('typobar.taskList'),
88
+ onClick: editorState.checkList,
89
+ },
90
+ {
91
+ type: 'divider',
92
+ },
82
93
  {
83
94
  active: editorState.isBlockquote,
84
95
  icon: MessageSquareQuote,
@@ -95,6 +106,12 @@ const TypoBar = memo(() => {
95
106
  {
96
107
  type: 'divider',
97
108
  },
109
+ {
110
+ icon: SigmaIcon,
111
+ key: 'math',
112
+ label: t('typobar.tex'),
113
+ onClick: editorState.insertMath,
114
+ },
98
115
  {
99
116
  active: editorState.isCode,
100
117
  icon: CodeXmlIcon,
@@ -9,6 +9,8 @@ export default {
9
9
  on: '显示格式工具栏',
10
10
  },
11
11
  },
12
+ cancel: '取消',
13
+ confirm: '确认',
12
14
  file: {
13
15
  error: '错误:{{message}}',
14
16
  uploading: '正在上传文件...',
@@ -22,6 +24,9 @@ export default {
22
24
  placeholder: '输入链接 URL',
23
25
  unlink: '取消链接',
24
26
  },
27
+ math: {
28
+ placeholder: '请输入 TeX 公式',
29
+ },
25
30
  table: {
26
31
  delete: '删除表格',
27
32
  deleteColumn: '删除列',
@@ -42,6 +47,8 @@ export default {
42
47
  numberList: '有序列表',
43
48
  strikethrough: '删除线',
44
49
  table: '插入表格',
50
+ taskList: '任务列表',
51
+ tex: 'TeX 公式',
45
52
  underline: '下划线',
46
53
  },
47
54
  };