@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.
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/editor.json +7 -0
- package/locales/bg-BG/editor.json +7 -0
- package/locales/de-DE/editor.json +7 -0
- package/locales/en-US/editor.json +7 -0
- package/locales/es-ES/editor.json +7 -0
- package/locales/fa-IR/editor.json +7 -0
- package/locales/fr-FR/editor.json +7 -0
- package/locales/it-IT/editor.json +7 -0
- package/locales/ja-JP/editor.json +7 -0
- package/locales/ko-KR/editor.json +7 -0
- package/locales/nl-NL/editor.json +7 -0
- package/locales/pl-PL/editor.json +7 -0
- package/locales/pt-BR/editor.json +7 -0
- package/locales/ru-RU/editor.json +7 -0
- package/locales/tr-TR/editor.json +7 -0
- package/locales/vi-VN/editor.json +7 -0
- package/locales/zh-CN/editor.json +7 -0
- package/locales/zh-TW/editor.json +7 -0
- package/package.json +2 -2
- package/packages/model-bank/src/aiModels/qwen.ts +4 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/ClassicChat.tsx +153 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/GroupChat.tsx +153 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/index.tsx +3 -145
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/ActionBar.tsx +30 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/Files/index.tsx +32 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/InputArea/Container.tsx +41 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/InputArea/index.tsx +156 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/Send.tsx +33 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/index.tsx +89 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/V1Mobile/useSend.ts +102 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/index.tsx +1 -1
- package/src/app/[variants]/(main)/settings/_layout/Mobile/Header.tsx +4 -0
- package/src/features/ChatInput/ActionBar/SaveTopic/index.tsx +4 -1
- package/src/features/ChatInput/InputEditor/index.tsx +20 -5
- package/src/features/ChatInput/TypoBar/index.tsx +17 -0
- 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
|
+
};
|
|
@@ -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
|
|
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:
|
|
120
|
-
|
|
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
|
};
|