@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.
- package/.env.example +5 -0
- package/CHANGELOG.md +58 -0
- package/Dockerfile +2 -0
- package/Dockerfile.database +2 -0
- package/Dockerfile.pglite +2 -0
- package/changelog/v1.json +21 -0
- package/docs/self-hosting/environment-variables/model-provider.mdx +18 -0
- package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +20 -0
- package/locales/ar/chat.json +8 -2
- package/locales/ar/editor.json +47 -0
- package/locales/bg-BG/chat.json +8 -2
- package/locales/bg-BG/editor.json +47 -0
- package/locales/de-DE/chat.json +8 -2
- package/locales/de-DE/editor.json +47 -0
- package/locales/en-US/chat.json +8 -2
- package/locales/en-US/editor.json +47 -0
- package/locales/es-ES/chat.json +8 -2
- package/locales/es-ES/editor.json +47 -0
- package/locales/es-ES/models.json +3 -1
- package/locales/fa-IR/chat.json +8 -2
- package/locales/fa-IR/editor.json +47 -0
- package/locales/fr-FR/chat.json +8 -2
- package/locales/fr-FR/editor.json +47 -0
- package/locales/it-IT/chat.json +8 -2
- package/locales/it-IT/editor.json +47 -0
- package/locales/ja-JP/chat.json +8 -2
- package/locales/ja-JP/editor.json +47 -0
- package/locales/ko-KR/chat.json +8 -2
- package/locales/ko-KR/editor.json +47 -0
- package/locales/ko-KR/models.json +3 -1
- package/locales/nl-NL/chat.json +8 -2
- package/locales/nl-NL/editor.json +47 -0
- package/locales/nl-NL/models.json +3 -1
- package/locales/pl-PL/chat.json +8 -2
- package/locales/pl-PL/editor.json +47 -0
- package/locales/pt-BR/chat.json +8 -2
- package/locales/pt-BR/editor.json +47 -0
- package/locales/ru-RU/chat.json +8 -2
- package/locales/ru-RU/editor.json +47 -0
- package/locales/tr-TR/chat.json +8 -2
- package/locales/tr-TR/editor.json +47 -0
- package/locales/vi-VN/chat.json +8 -2
- package/locales/vi-VN/editor.json +47 -0
- package/locales/zh-CN/chat.json +8 -2
- package/locales/zh-CN/editor.json +47 -0
- package/locales/zh-CN/modelProvider.json +1 -1
- package/locales/zh-TW/chat.json +8 -2
- package/locales/zh-TW/editor.json +47 -0
- package/locales/zh-TW/models.json +3 -1
- package/next.config.ts +4 -0
- package/package.json +4 -2
- package/packages/const/src/layoutTokens.ts +1 -0
- package/packages/model-bank/src/aiModels/aihubmix.ts +38 -4
- package/packages/model-bank/src/aiModels/groq.ts +26 -8
- package/packages/model-bank/src/aiModels/hunyuan.ts +3 -3
- package/packages/model-bank/src/aiModels/modelscope.ts +13 -2
- package/packages/model-bank/src/aiModels/moonshot.ts +25 -5
- package/packages/model-bank/src/aiModels/novita.ts +40 -9
- package/packages/model-bank/src/aiModels/openrouter.ts +0 -13
- package/packages/model-bank/src/aiModels/qwen.ts +62 -1
- package/packages/model-bank/src/aiModels/siliconcloud.ts +20 -0
- package/packages/model-bank/src/aiModels/volcengine.ts +141 -15
- package/packages/model-runtime/src/newapi/index.test.ts +49 -42
- package/packages/model-runtime/src/newapi/index.ts +124 -143
- package/packages/types/src/index.ts +1 -0
- package/packages/utils/src/index.ts +1 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/{Footer/MessageFromUrl.tsx → MessageFromUrl.tsx} +3 -2
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/index.tsx +129 -28
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/index.tsx +44 -66
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/useSend.ts +141 -0
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/Content.tsx +7 -1
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/InboxWelcome/QuestionSuggest.tsx +3 -2
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatList/WelcomeChatItem/OpeningQuestions.tsx +3 -2
- package/src/app/[variants]/(main)/chat/(workspace)/_layout/Desktop/ChatHeader/HeaderAction.tsx +18 -2
- package/src/app/[variants]/(main)/settings/provider/(detail)/newapi/page.tsx +1 -1
- package/src/config/llm.ts +8 -0
- package/src/features/ChatInput/ActionBar/STT/common.tsx +41 -47
- package/src/features/ChatInput/{Topic → ActionBar/SaveTopic}/index.tsx +15 -4
- package/src/features/ChatInput/ActionBar/Typo/index.tsx +22 -0
- package/src/features/ChatInput/ActionBar/components/Action.tsx +4 -0
- package/src/features/ChatInput/ActionBar/config.ts +7 -1
- package/src/features/ChatInput/ActionBar/index.tsx +40 -51
- package/src/features/ChatInput/ChatInputProvider.tsx +54 -0
- package/src/features/ChatInput/Desktop/FilePreview/FileItem/index.tsx +20 -11
- package/src/features/ChatInput/Desktop/FilePreview/FileList.tsx +16 -15
- package/src/features/ChatInput/Desktop/index.tsx +94 -69
- package/src/features/ChatInput/InputEditor/index.tsx +134 -0
- package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/File.tsx +1 -2
- package/src/features/ChatInput/Mobile/FilePreview/index.tsx +44 -0
- package/src/features/ChatInput/Mobile/index.tsx +72 -0
- package/src/features/ChatInput/SendArea/ExpandButton.tsx +30 -0
- package/src/features/ChatInput/SendArea/SendButton.tsx +29 -0
- package/src/features/ChatInput/SendArea/ShortcutHint.tsx +52 -0
- package/src/features/ChatInput/SendArea/index.tsx +36 -0
- package/src/features/ChatInput/StoreUpdater.tsx +41 -0
- package/src/features/ChatInput/TypoBar/index.tsx +139 -0
- package/src/features/ChatInput/hooks/useChatInputEditor.ts +36 -0
- package/src/features/ChatInput/index.ts +7 -0
- package/src/features/ChatInput/store/action.ts +75 -0
- package/src/features/ChatInput/store/index.ts +23 -0
- package/src/features/ChatInput/store/initialState.ts +54 -0
- package/src/features/ChatInput/store/selectors.ts +5 -0
- package/src/features/Conversation/components/BackBottom/style.ts +1 -1
- package/src/features/Conversation/components/SkeletonList.tsx +10 -3
- package/src/features/Conversation/components/VirtualizedList/index.tsx +53 -44
- package/src/features/Conversation/components/WideScreenContainer/index.tsx +43 -0
- package/src/features/Portal/Thread/Chat/ChatInput/index.tsx +49 -42
- package/src/features/Portal/Thread/Chat/ChatInput/useSend.ts +48 -22
- package/src/features/Portal/Thread/Chat/index.tsx +2 -2
- package/src/features/Portal/Thread/Header/index.tsx +1 -1
- package/src/hooks/useHotkeys/chatScope.ts +5 -3
- package/src/layout/GlobalProvider/Editor.tsx +27 -0
- package/src/layout/GlobalProvider/Locale.tsx +3 -23
- package/src/locales/default/chat.ts +8 -2
- package/src/locales/default/editor.ts +47 -0
- package/src/locales/default/index.ts +2 -0
- package/src/locales/default/modelProvider.ts +1 -1
- package/src/services/aiChat.ts +8 -2
- package/src/store/chat/slices/aiChat/actions/__tests__/cancel-functionality.test.ts +107 -0
- package/src/store/chat/slices/aiChat/actions/__tests__/generateAIChatV2.test.ts +394 -40
- package/src/store/chat/slices/aiChat/actions/generateAIChatV2.ts +175 -35
- package/src/store/chat/slices/aiChat/initialState.ts +19 -0
- package/src/store/chat/slices/aiChat/selectors.ts +18 -0
- package/src/store/global/action.test.ts +6 -5
- package/src/store/global/actions/__tests__/general.test.ts +6 -6
- package/src/store/global/actions/workspacePane.ts +6 -0
- package/src/store/global/initialState.ts +2 -4
- package/src/store/global/selectors/systemStatus.test.ts +1 -2
- package/src/store/global/selectors/systemStatus.ts +2 -5
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/SendMore.tsx +0 -104
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/ShortcutHint.tsx +0 -40
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/Footer/index.tsx +0 -125
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.test.tsx +0 -332
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.tsx +0 -29
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files/index.tsx +0 -33
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/Container.tsx +0 -41
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/index.tsx +0 -156
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Send.tsx +0 -33
- package/src/features/ChatInput/Desktop/Header/index.tsx +0 -30
- package/src/features/ChatInput/Desktop/InputArea/index.tsx +0 -143
- package/src/features/ChatInput/Desktop/__tests__/useAutoFocus.test.ts +0 -45
- package/src/features/ChatInput/Desktop/useAutoFocus.ts +0 -13
- package/src/features/ChatInput/useSend.ts +0 -102
- package/src/features/Portal/Thread/Chat/ChatInput/Footer.tsx +0 -90
- package/src/features/Portal/Thread/Chat/ChatInput/TextArea.tsx +0 -30
- package/src/libs/trpc/client/types.ts +0 -18
- /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/Image.tsx +0 -0
- /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/index.tsx +0 -0
- /package/src/{app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Files → features/ChatInput/Mobile/FilePreview}/FileItem/style.ts +0 -0
@@ -1,156 +0,0 @@
|
|
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;
|
package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/Send.tsx
DELETED
@@ -1,33 +0,0 @@
|
|
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;
|
@@ -1,30 +0,0 @@
|
|
1
|
-
import { ActionIcon } from '@lobehub/ui';
|
2
|
-
import { Maximize2, Minimize2 } from 'lucide-react';
|
3
|
-
import { memo } from 'react';
|
4
|
-
|
5
|
-
import ActionBar from '@/features/ChatInput/ActionBar';
|
6
|
-
import { ActionKeys } from '@/features/ChatInput/types';
|
7
|
-
|
8
|
-
interface HeaderProps {
|
9
|
-
expand: boolean;
|
10
|
-
leftActions: ActionKeys[];
|
11
|
-
rightActions: ActionKeys[];
|
12
|
-
setExpand: (expand: boolean) => void;
|
13
|
-
}
|
14
|
-
|
15
|
-
const Header = memo<HeaderProps>(({ expand, setExpand, leftActions, rightActions }) => (
|
16
|
-
<ActionBar
|
17
|
-
leftActions={leftActions}
|
18
|
-
rightActions={rightActions}
|
19
|
-
rightAreaEndRender={
|
20
|
-
<ActionIcon
|
21
|
-
icon={expand ? Minimize2 : Maximize2}
|
22
|
-
onClick={() => {
|
23
|
-
setExpand(!expand);
|
24
|
-
}}
|
25
|
-
/>
|
26
|
-
}
|
27
|
-
/>
|
28
|
-
));
|
29
|
-
|
30
|
-
export default Header;
|
@@ -1,143 +0,0 @@
|
|
1
|
-
import { TextArea } from '@lobehub/ui';
|
2
|
-
import { createStyles } from 'antd-style';
|
3
|
-
import { TextAreaRef } from 'antd/es/input/TextArea';
|
4
|
-
import { RefObject, memo, useEffect, useRef } from 'react';
|
5
|
-
import { useHotkeysContext } from 'react-hotkeys-hook';
|
6
|
-
import { useTranslation } from 'react-i18next';
|
7
|
-
|
8
|
-
import { isDesktop } from '@/const/version';
|
9
|
-
import { useUserStore } from '@/store/user';
|
10
|
-
import { preferenceSelectors } from '@/store/user/selectors';
|
11
|
-
import { HotkeyEnum } from '@/types/hotkey';
|
12
|
-
import { isCommandPressed } from '@/utils/keyboard';
|
13
|
-
|
14
|
-
import { useAutoFocus } from '../useAutoFocus';
|
15
|
-
|
16
|
-
const useStyles = createStyles(({ css }) => {
|
17
|
-
return {
|
18
|
-
textarea: css`
|
19
|
-
resize: none !important;
|
20
|
-
|
21
|
-
height: 100% !important;
|
22
|
-
padding-block: 0;
|
23
|
-
padding-inline: 16px;
|
24
|
-
|
25
|
-
line-height: 1.5;
|
26
|
-
|
27
|
-
box-shadow: none !important;
|
28
|
-
`,
|
29
|
-
textareaContainer: css`
|
30
|
-
position: relative;
|
31
|
-
flex: 1;
|
32
|
-
`,
|
33
|
-
};
|
34
|
-
});
|
35
|
-
|
36
|
-
interface InputAreaProps {
|
37
|
-
loading?: boolean;
|
38
|
-
onChange: (string: string) => void;
|
39
|
-
onSend: () => void;
|
40
|
-
value: string;
|
41
|
-
}
|
42
|
-
|
43
|
-
const InputArea = memo<InputAreaProps>(({ onSend, value, loading, onChange }) => {
|
44
|
-
const { enableScope, disableScope } = useHotkeysContext();
|
45
|
-
const { t } = useTranslation('chat');
|
46
|
-
const { styles } = useStyles();
|
47
|
-
|
48
|
-
const ref = useRef<TextAreaRef>(null);
|
49
|
-
const isChineseInput = useRef(false);
|
50
|
-
|
51
|
-
const useCmdEnterToSend = useUserStore(preferenceSelectors.useCmdEnterToSend);
|
52
|
-
|
53
|
-
useAutoFocus(ref as RefObject<TextAreaRef>);
|
54
|
-
|
55
|
-
const hasValue = !!value;
|
56
|
-
|
57
|
-
useEffect(() => {
|
58
|
-
const fn = (e: BeforeUnloadEvent) => {
|
59
|
-
if (hasValue) {
|
60
|
-
// set returnValue to trigger alert modal
|
61
|
-
// Note: No matter what value is set, the browser will display the standard text
|
62
|
-
e.returnValue = '你有正在输入中的内容,确定要离开吗?';
|
63
|
-
}
|
64
|
-
};
|
65
|
-
|
66
|
-
window.addEventListener('beforeunload', fn);
|
67
|
-
return () => {
|
68
|
-
window.removeEventListener('beforeunload', fn);
|
69
|
-
};
|
70
|
-
}, [hasValue]);
|
71
|
-
|
72
|
-
return (
|
73
|
-
<div className={styles.textareaContainer}>
|
74
|
-
<TextArea
|
75
|
-
autoFocus
|
76
|
-
className={styles.textarea}
|
77
|
-
onBlur={(e) => {
|
78
|
-
onChange?.(e.target.value);
|
79
|
-
disableScope(HotkeyEnum.AddUserMessage);
|
80
|
-
}}
|
81
|
-
onChange={(e) => {
|
82
|
-
onChange?.(e.target.value);
|
83
|
-
}}
|
84
|
-
onCompositionEnd={() => {
|
85
|
-
isChineseInput.current = false;
|
86
|
-
}}
|
87
|
-
onCompositionStart={() => {
|
88
|
-
isChineseInput.current = true;
|
89
|
-
}}
|
90
|
-
onContextMenu={async (e) => {
|
91
|
-
if (isDesktop) {
|
92
|
-
e.preventDefault();
|
93
|
-
const textArea = ref.current?.resizableTextArea?.textArea;
|
94
|
-
const hasSelection = textArea && textArea.selectionStart !== textArea.selectionEnd;
|
95
|
-
const { electronSystemService } = await import('@/services/electron/system');
|
96
|
-
|
97
|
-
electronSystemService.showContextMenu('editor', {
|
98
|
-
hasSelection: !!hasSelection,
|
99
|
-
value: value,
|
100
|
-
});
|
101
|
-
}
|
102
|
-
}}
|
103
|
-
onFocus={() => {
|
104
|
-
enableScope(HotkeyEnum.AddUserMessage);
|
105
|
-
}}
|
106
|
-
onPressEnter={(e) => {
|
107
|
-
if (loading || e.altKey || e.shiftKey || isChineseInput.current) return;
|
108
|
-
|
109
|
-
// eslint-disable-next-line unicorn/consistent-function-scoping
|
110
|
-
const send = () => {
|
111
|
-
// avoid inserting newline when sending message.
|
112
|
-
// refs: https://github.com/lobehub/lobe-chat/pull/989
|
113
|
-
e.preventDefault();
|
114
|
-
|
115
|
-
onSend();
|
116
|
-
};
|
117
|
-
const commandKey = isCommandPressed(e);
|
118
|
-
|
119
|
-
// when user like cmd + enter to send message
|
120
|
-
if (useCmdEnterToSend) {
|
121
|
-
if (commandKey) send();
|
122
|
-
} else {
|
123
|
-
// cmd + enter to wrap
|
124
|
-
if (commandKey) {
|
125
|
-
onChange?.((e.target as any).value + '\n');
|
126
|
-
return;
|
127
|
-
}
|
128
|
-
|
129
|
-
send();
|
130
|
-
}
|
131
|
-
}}
|
132
|
-
placeholder={t('sendPlaceholder')}
|
133
|
-
ref={ref}
|
134
|
-
value={value}
|
135
|
-
variant={'borderless'}
|
136
|
-
/>
|
137
|
-
</div>
|
138
|
-
);
|
139
|
-
});
|
140
|
-
|
141
|
-
InputArea.displayName = 'DesktopInputArea';
|
142
|
-
|
143
|
-
export default InputArea;
|
@@ -1,45 +0,0 @@
|
|
1
|
-
import { act, renderHook } from '@testing-library/react';
|
2
|
-
import { describe, expect, it, vi } from 'vitest';
|
3
|
-
|
4
|
-
import { useChatStore } from '@/store/chat';
|
5
|
-
import { chatSelectors } from '@/store/chat/selectors';
|
6
|
-
|
7
|
-
import { useAutoFocus } from '../useAutoFocus';
|
8
|
-
|
9
|
-
vi.mock('zustand/traditional');
|
10
|
-
|
11
|
-
describe('useAutoFocus', () => {
|
12
|
-
it('should focus the input when chatKey changes', () => {
|
13
|
-
const focusMock = vi.fn();
|
14
|
-
const inputRef = { current: { focus: focusMock } };
|
15
|
-
|
16
|
-
act(() => {
|
17
|
-
useChatStore.setState({ activeId: '1', activeTopicId: '2' });
|
18
|
-
});
|
19
|
-
|
20
|
-
renderHook(() => useAutoFocus(inputRef as any));
|
21
|
-
|
22
|
-
expect(focusMock).toHaveBeenCalledTimes(1);
|
23
|
-
|
24
|
-
act(() => {
|
25
|
-
useChatStore.setState({ activeId: '1', activeTopicId: '3' });
|
26
|
-
});
|
27
|
-
|
28
|
-
renderHook(() => useAutoFocus(inputRef as any));
|
29
|
-
|
30
|
-
// I don't know why its 3, but is large than 2 is fine
|
31
|
-
expect(focusMock).toHaveBeenCalledTimes(3);
|
32
|
-
});
|
33
|
-
|
34
|
-
it('should not focus the input if inputRef is not available', () => {
|
35
|
-
const inputRef = { current: null };
|
36
|
-
|
37
|
-
act(() => {
|
38
|
-
useChatStore.setState({ activeId: '1', activeTopicId: '2' });
|
39
|
-
});
|
40
|
-
|
41
|
-
renderHook(() => useAutoFocus(inputRef as any));
|
42
|
-
|
43
|
-
expect(inputRef.current).toBeNull();
|
44
|
-
});
|
45
|
-
});
|
@@ -1,13 +0,0 @@
|
|
1
|
-
import { TextAreaRef } from 'antd/es/input/TextArea';
|
2
|
-
import { RefObject, useEffect } from 'react';
|
3
|
-
|
4
|
-
import { useChatStore } from '@/store/chat';
|
5
|
-
import { chatSelectors } from '@/store/chat/selectors';
|
6
|
-
|
7
|
-
export const useAutoFocus = (inputRef: RefObject<TextAreaRef>) => {
|
8
|
-
const chatKey = useChatStore(chatSelectors.currentChatKey);
|
9
|
-
|
10
|
-
useEffect(() => {
|
11
|
-
inputRef.current?.focus();
|
12
|
-
}, [chatKey]);
|
13
|
-
};
|
@@ -1,102 +0,0 @@
|
|
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,90 +0,0 @@
|
|
1
|
-
import { Button } from '@lobehub/ui';
|
2
|
-
import { createStyles } from 'antd-style';
|
3
|
-
import { rgba } from 'polished';
|
4
|
-
import { memo } from 'react';
|
5
|
-
import { useTranslation } from 'react-i18next';
|
6
|
-
import { Flexbox } from 'react-layout-kit';
|
7
|
-
|
8
|
-
import StopLoadingIcon from '@/components/StopLoading';
|
9
|
-
import { useChatStore } from '@/store/chat';
|
10
|
-
import { threadSelectors } from '@/store/chat/selectors';
|
11
|
-
|
12
|
-
import { useSendThreadMessage } from './useSend';
|
13
|
-
|
14
|
-
const useStyles = createStyles(({ css, prefixCls, token }) => {
|
15
|
-
return {
|
16
|
-
loadingButton: css`
|
17
|
-
display: flex;
|
18
|
-
align-items: center;
|
19
|
-
`,
|
20
|
-
overrideAntdIcon: css`
|
21
|
-
.${prefixCls}-btn.${prefixCls}-btn-icon-only {
|
22
|
-
display: flex;
|
23
|
-
align-items: center;
|
24
|
-
justify-content: center;
|
25
|
-
}
|
26
|
-
|
27
|
-
.${prefixCls}-btn.${prefixCls}-dropdown-trigger {
|
28
|
-
&::before {
|
29
|
-
background-color: ${rgba(token.colorBgLayout, 0.1)} !important;
|
30
|
-
}
|
31
|
-
}
|
32
|
-
`,
|
33
|
-
};
|
34
|
-
});
|
35
|
-
|
36
|
-
interface FooterProps {
|
37
|
-
onExpandChange: (expand: boolean) => void;
|
38
|
-
}
|
39
|
-
|
40
|
-
const Footer = memo<FooterProps>(({ onExpandChange }) => {
|
41
|
-
const { t } = useTranslation('chat');
|
42
|
-
|
43
|
-
const { styles } = useStyles();
|
44
|
-
|
45
|
-
const [isAIGenerating, stopGenerateMessage] = useChatStore((s) => [
|
46
|
-
threadSelectors.isThreadAIGenerating(s),
|
47
|
-
s.stopGenerateMessage,
|
48
|
-
]);
|
49
|
-
|
50
|
-
const { send: sendMessage, canSend } = useSendThreadMessage();
|
51
|
-
|
52
|
-
return (
|
53
|
-
<Flexbox
|
54
|
-
align={'end'}
|
55
|
-
className={styles.overrideAntdIcon}
|
56
|
-
distribution={'space-between'}
|
57
|
-
flex={'none'}
|
58
|
-
gap={8}
|
59
|
-
horizontal
|
60
|
-
paddingInline={16}
|
61
|
-
>
|
62
|
-
<div />
|
63
|
-
{isAIGenerating ? (
|
64
|
-
<Button
|
65
|
-
className={styles.loadingButton}
|
66
|
-
icon={<StopLoadingIcon />}
|
67
|
-
onClick={stopGenerateMessage}
|
68
|
-
>
|
69
|
-
{t('input.stop')}
|
70
|
-
</Button>
|
71
|
-
) : (
|
72
|
-
<Button
|
73
|
-
disabled={!canSend}
|
74
|
-
loading={!canSend}
|
75
|
-
onClick={() => {
|
76
|
-
sendMessage();
|
77
|
-
onExpandChange?.(false);
|
78
|
-
}}
|
79
|
-
type={'primary'}
|
80
|
-
>
|
81
|
-
{t('input.send')}
|
82
|
-
</Button>
|
83
|
-
)}
|
84
|
-
</Flexbox>
|
85
|
-
);
|
86
|
-
});
|
87
|
-
|
88
|
-
Footer.displayName = 'Footer';
|
89
|
-
|
90
|
-
export default Footer;
|
@@ -1,30 +0,0 @@
|
|
1
|
-
import { memo } from 'react';
|
2
|
-
|
3
|
-
import InputArea from '@/features/ChatInput/Desktop/InputArea';
|
4
|
-
import { useChatStore } from '@/store/chat';
|
5
|
-
import { chatSelectors } from '@/store/chat/selectors';
|
6
|
-
|
7
|
-
import { useSendThreadMessage } from './useSend';
|
8
|
-
|
9
|
-
const TextArea = memo<{ onSend?: () => void }>(({ onSend }) => {
|
10
|
-
const [loading, value, updateInputMessage] = useChatStore((s) => [
|
11
|
-
chatSelectors.isAIGenerating(s),
|
12
|
-
s.threadInputMessage,
|
13
|
-
s.updateThreadInputMessage,
|
14
|
-
]);
|
15
|
-
const { send: sendMessage } = useSendThreadMessage();
|
16
|
-
|
17
|
-
return (
|
18
|
-
<InputArea
|
19
|
-
loading={loading}
|
20
|
-
onChange={updateInputMessage}
|
21
|
-
onSend={() => {
|
22
|
-
sendMessage();
|
23
|
-
onSend?.();
|
24
|
-
}}
|
25
|
-
value={value}
|
26
|
-
/>
|
27
|
-
);
|
28
|
-
});
|
29
|
-
|
30
|
-
export default TextArea;
|
@@ -1,18 +0,0 @@
|
|
1
|
-
export type ErrorResponse = ErrorItem[];
|
2
|
-
|
3
|
-
export interface ErrorItem {
|
4
|
-
error: {
|
5
|
-
json: {
|
6
|
-
code: number;
|
7
|
-
data: Data;
|
8
|
-
message: string;
|
9
|
-
};
|
10
|
-
};
|
11
|
-
}
|
12
|
-
|
13
|
-
export interface Data {
|
14
|
-
code: string;
|
15
|
-
httpStatus: number;
|
16
|
-
path: string;
|
17
|
-
stack: string;
|
18
|
-
}
|
File without changes
|
File without changes
|