@lobehub/chat 0.100.5 → 0.101.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/.eslintrc.js +1 -0
- package/CHANGELOG.md +25 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/__mocks__/zustand/traditional.ts +25 -0
- package/package.json +7 -3
- package/src/app/api/files/image/imgur.ts +72 -0
- package/src/app/api/files/image/route.ts +42 -0
- package/src/app/api/openai/chat/createChatCompletion.ts +2 -1
- package/src/app/chat/(desktop)/features/ChatInput/DragUpload.tsx +166 -0
- package/src/app/chat/(desktop)/features/ChatInput/Footer/LocalFiles.tsx +10 -0
- package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +64 -0
- package/src/app/chat/(desktop)/features/ChatInput/InputArea.tsx +3 -3
- package/src/app/chat/(desktop)/features/ChatInput/index.tsx +38 -30
- package/src/app/chat/components/FileList/FileItem.style.ts +81 -0
- package/src/app/chat/components/FileList/FileItem.tsx +69 -0
- package/src/app/chat/components/FileList/Lightbox.tsx +46 -0
- package/src/app/chat/components/FileList/index.tsx +63 -0
- package/src/app/chat/features/ChatInput/ActionBar/Clear.tsx +13 -4
- package/src/app/chat/features/ChatInput/ActionBar/FileUpload.tsx +53 -0
- package/src/app/chat/features/ChatInput/ActionBar/config.ts +3 -1
- package/src/app/chat/{(desktop)/features → features}/ChatInput/useSend.ts +5 -1
- package/src/app/chat/features/Conversation/ChatList/Messages/User.tsx +23 -0
- package/src/app/chat/features/Conversation/ChatList/Messages/index.ts +2 -0
- package/src/config/__tests__/client.test.ts +76 -0
- package/src/config/__tests__/server.test.ts +52 -0
- package/src/config/server.ts +8 -0
- package/src/const/layoutTokens.ts +1 -1
- package/src/const/llm.ts +3 -0
- package/src/const/settings.ts +2 -0
- package/src/database/core/__tests__/db.test.ts +77 -0
- package/src/database/core/__tests__/model.test.ts +55 -0
- package/src/database/core/db.ts +25 -0
- package/src/database/core/index.ts +1 -0
- package/src/database/core/model.ts +49 -0
- package/src/database/core/schema.ts +9 -0
- package/src/database/models/file.test.ts +71 -0
- package/src/database/models/file.ts +26 -0
- package/src/locales/default/chat.ts +5 -0
- package/src/services/__tests__/file.test.ts +88 -0
- package/src/services/chatModel.ts +11 -6
- package/src/services/file.ts +36 -0
- package/src/store/files/index.ts +2 -0
- package/src/store/files/initialState.ts +7 -0
- package/src/store/files/selectors.test.ts +75 -0
- package/src/store/files/selectors.ts +34 -0
- package/src/store/files/slices/images/action.test.ts +186 -0
- package/src/store/files/slices/images/action.ts +83 -0
- package/src/store/files/slices/images/index.ts +3 -0
- package/src/store/files/slices/images/initialState.ts +11 -0
- package/src/store/files/store.ts +27 -0
- package/src/store/global/selectors/settings.test.ts +1 -3
- package/src/store/session/slices/agentConfig/selectors.ts +7 -1
- package/src/store/session/slices/chat/actions/message.ts +78 -17
- package/src/store/session/slices/chat/reducers/files.test.ts +38 -0
- package/src/store/session/slices/chat/reducers/files.ts +37 -0
- package/src/store/session/slices/session/initialState.ts +1 -0
- package/src/store/session/slices/session/reducers/session.test.ts +98 -1
- package/src/store/session/slices/session/reducers/session.ts +26 -1
- package/src/types/chatMessage.ts +3 -2
- package/src/types/database/db.ts +4 -0
- package/src/types/database/files.ts +38 -0
- package/src/types/files.ts +8 -0
- package/src/types/openai/chat.ts +15 -1
- package/src/types/session.ts +1 -0
- package/tests/setup.ts +8 -0
- package/vitest.config.ts +2 -0
- package/src/app/chat/(desktop)/features/ChatInput/Footer.tsx +0 -63
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createStyles } from 'antd-style';
|
|
2
|
+
|
|
3
|
+
export const IMAGE_SIZE = 64;
|
|
4
|
+
const imageBorderRaidus = 8;
|
|
5
|
+
|
|
6
|
+
export const useStyles = createStyles(({ css, cx, token, isDarkMode }) => {
|
|
7
|
+
const closeIcon = cx(css`
|
|
8
|
+
cursor: pointer;
|
|
9
|
+
|
|
10
|
+
position: absolute;
|
|
11
|
+
top: -6px;
|
|
12
|
+
right: -6px;
|
|
13
|
+
|
|
14
|
+
width: 16px;
|
|
15
|
+
height: 16px;
|
|
16
|
+
|
|
17
|
+
color: ${isDarkMode ? token.colorTextQuaternary : token.colorTextTertiary};
|
|
18
|
+
|
|
19
|
+
opacity: 0;
|
|
20
|
+
background: ${isDarkMode ? token.colorTextSecondary : token.colorBgContainer};
|
|
21
|
+
border-radius: 50%;
|
|
22
|
+
|
|
23
|
+
transition: all 0.2s ease-in;
|
|
24
|
+
|
|
25
|
+
&:hover {
|
|
26
|
+
scale: 1.2;
|
|
27
|
+
}
|
|
28
|
+
`);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
closeIcon,
|
|
32
|
+
container: css`
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
|
|
35
|
+
position: relative;
|
|
36
|
+
|
|
37
|
+
width: ${IMAGE_SIZE}px;
|
|
38
|
+
height: ${IMAGE_SIZE}px;
|
|
39
|
+
|
|
40
|
+
border-radius: ${imageBorderRaidus}px;
|
|
41
|
+
|
|
42
|
+
&:hover {
|
|
43
|
+
.${closeIcon} {
|
|
44
|
+
opacity: 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
`,
|
|
48
|
+
image: css`
|
|
49
|
+
opacity: ${isDarkMode ? 0.6 : 1};
|
|
50
|
+
object-fit: cover;
|
|
51
|
+
border-radius: 8px;
|
|
52
|
+
animation: fade-in 0.3s ease-in;
|
|
53
|
+
|
|
54
|
+
@keyframes fade-in {
|
|
55
|
+
from {
|
|
56
|
+
scale: 1.2;
|
|
57
|
+
opacity: 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
to {
|
|
61
|
+
scale: 1;
|
|
62
|
+
opacity: 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
`,
|
|
66
|
+
imageCtn: css`
|
|
67
|
+
position: relative;
|
|
68
|
+
`,
|
|
69
|
+
imageWrapper: css`
|
|
70
|
+
overflow: hidden;
|
|
71
|
+
width: ${IMAGE_SIZE}px;
|
|
72
|
+
height: ${IMAGE_SIZE}px;
|
|
73
|
+
border-radius: ${imageBorderRaidus}px;
|
|
74
|
+
`,
|
|
75
|
+
notFound: css`
|
|
76
|
+
color: ${token.colorTextSecondary};
|
|
77
|
+
background: ${token.colorFillTertiary};
|
|
78
|
+
border-radius: ${imageBorderRaidus}px;
|
|
79
|
+
`,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { CloseCircleFilled } from '@ant-design/icons';
|
|
2
|
+
import { Icon } from '@lobehub/ui';
|
|
3
|
+
import { Skeleton } from 'antd';
|
|
4
|
+
import { LucideImageOff } from 'lucide-react';
|
|
5
|
+
import Image from 'next/image';
|
|
6
|
+
import { memo } from 'react';
|
|
7
|
+
import { Center, Flexbox } from 'react-layout-kit';
|
|
8
|
+
|
|
9
|
+
import { useFileStore } from '@/store/files';
|
|
10
|
+
|
|
11
|
+
import { IMAGE_SIZE, useStyles } from './FileItem.style';
|
|
12
|
+
|
|
13
|
+
const FileItem = memo<{ editable: boolean; id: string; onClick: () => void }>(
|
|
14
|
+
({ editable, id, onClick }) => {
|
|
15
|
+
const { styles } = useStyles();
|
|
16
|
+
const [useFetchFile, removeFile] = useFileStore((s) => [s.useFetchFile, s.removeFile]);
|
|
17
|
+
|
|
18
|
+
const { data, isLoading } = useFetchFile(id);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Flexbox className={styles.container} onClick={onClick}>
|
|
22
|
+
{isLoading ? (
|
|
23
|
+
<Skeleton
|
|
24
|
+
active
|
|
25
|
+
title={{
|
|
26
|
+
style: { borderRadius: 8, height: IMAGE_SIZE },
|
|
27
|
+
width: IMAGE_SIZE,
|
|
28
|
+
}}
|
|
29
|
+
/>
|
|
30
|
+
) : (
|
|
31
|
+
<Flexbox className={styles.imageCtn}>
|
|
32
|
+
<div className={styles.imageWrapper}>
|
|
33
|
+
{data ? (
|
|
34
|
+
<Image
|
|
35
|
+
alt={data.name || ''}
|
|
36
|
+
className={styles.image}
|
|
37
|
+
fetchPriority={'high'}
|
|
38
|
+
height={IMAGE_SIZE}
|
|
39
|
+
loading={'lazy'}
|
|
40
|
+
src={data.url}
|
|
41
|
+
width={IMAGE_SIZE}
|
|
42
|
+
/>
|
|
43
|
+
) : (
|
|
44
|
+
<Center className={styles.notFound} height={'100%'}>
|
|
45
|
+
<Icon icon={LucideImageOff} size={{ fontSize: 28 }} />
|
|
46
|
+
</Center>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
</Flexbox>
|
|
50
|
+
)}
|
|
51
|
+
{/* only show close icon when editable */}
|
|
52
|
+
{editable && (
|
|
53
|
+
<Center
|
|
54
|
+
className={styles.closeIcon}
|
|
55
|
+
onClick={(e) => {
|
|
56
|
+
e.stopPropagation();
|
|
57
|
+
|
|
58
|
+
removeFile(id);
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
<CloseCircleFilled />
|
|
62
|
+
</Center>
|
|
63
|
+
)}
|
|
64
|
+
</Flexbox>
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
export default FileItem;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createStyles } from 'antd-style';
|
|
2
|
+
import { memo } from 'react';
|
|
3
|
+
import Lightbox from 'react-spring-lightbox';
|
|
4
|
+
|
|
5
|
+
import { filesSelectors, useFileStore } from '@/store/files';
|
|
6
|
+
|
|
7
|
+
const useStyles = createStyles(({ css, token }) => ({
|
|
8
|
+
wrapper: css`
|
|
9
|
+
background: ${token.colorBgMask};
|
|
10
|
+
backdrop-filter: blur(4px);
|
|
11
|
+
`,
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
interface LightBoxProps {
|
|
15
|
+
index: number;
|
|
16
|
+
items: string[];
|
|
17
|
+
onNext: () => void;
|
|
18
|
+
onOpenChange: (open: boolean) => void;
|
|
19
|
+
onPrev: () => void;
|
|
20
|
+
open: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const LightBox = memo<LightBoxProps>(({ onOpenChange, open, items, index, onNext, onPrev }) => {
|
|
24
|
+
const { styles } = useStyles();
|
|
25
|
+
|
|
26
|
+
const list = useFileStore(filesSelectors.getImageDetailByList(items));
|
|
27
|
+
return (
|
|
28
|
+
<Lightbox
|
|
29
|
+
className={styles.wrapper}
|
|
30
|
+
currentIndex={index}
|
|
31
|
+
images={list.map((i) => ({
|
|
32
|
+
alt: i.name,
|
|
33
|
+
loading: 'lazy',
|
|
34
|
+
src: i.url,
|
|
35
|
+
}))}
|
|
36
|
+
isOpen={open}
|
|
37
|
+
onClose={() => {
|
|
38
|
+
onOpenChange(false);
|
|
39
|
+
}}
|
|
40
|
+
onNext={onNext}
|
|
41
|
+
onPrev={onPrev}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export default LightBox;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { memo, useState } from 'react';
|
|
2
|
+
import { Flexbox } from 'react-layout-kit';
|
|
3
|
+
|
|
4
|
+
import FileItem from './FileItem';
|
|
5
|
+
import Lightbox from './Lightbox';
|
|
6
|
+
|
|
7
|
+
// TODO:后续看情况是否做图片过多的滚动功能
|
|
8
|
+
// const useStyles = createStyles(({ css }) => ({
|
|
9
|
+
// container: css`
|
|
10
|
+
// width: ${IMAGE_SIZE * 5 + 4 * 8 + IMAGE_SIZE / 2}px;
|
|
11
|
+
// overflow-x: scroll;
|
|
12
|
+
// height: ${IMAGE_SIZE + 12}px;
|
|
13
|
+
// `,
|
|
14
|
+
// }));
|
|
15
|
+
|
|
16
|
+
interface FileListProps {
|
|
17
|
+
editable?: boolean;
|
|
18
|
+
items: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const FileList = memo<FileListProps>(({ items, editable = true }) => {
|
|
22
|
+
// const { styles } = useStyles();
|
|
23
|
+
|
|
24
|
+
const [showLightbox, setShowLightbox] = useState(false);
|
|
25
|
+
const [currentImageIndex, setCurrentIndex] = useState(0);
|
|
26
|
+
|
|
27
|
+
const gotoPrevious = () => currentImageIndex > 0 && setCurrentIndex(currentImageIndex - 1);
|
|
28
|
+
const gotoNext = () =>
|
|
29
|
+
currentImageIndex + 1 < items.length && setCurrentIndex(currentImageIndex + 1);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<Flexbox
|
|
34
|
+
align={'flex-end'}
|
|
35
|
+
// className={styles.container}
|
|
36
|
+
gap={8}
|
|
37
|
+
horizontal
|
|
38
|
+
>
|
|
39
|
+
{items.map((i, index) => (
|
|
40
|
+
<FileItem
|
|
41
|
+
editable={editable}
|
|
42
|
+
id={i}
|
|
43
|
+
key={i}
|
|
44
|
+
onClick={() => {
|
|
45
|
+
setCurrentIndex(index);
|
|
46
|
+
setShowLightbox(true);
|
|
47
|
+
}}
|
|
48
|
+
/>
|
|
49
|
+
))}
|
|
50
|
+
</Flexbox>
|
|
51
|
+
<Lightbox
|
|
52
|
+
index={currentImageIndex}
|
|
53
|
+
items={items}
|
|
54
|
+
onNext={gotoNext}
|
|
55
|
+
onOpenChange={setShowLightbox}
|
|
56
|
+
onPrev={gotoPrevious}
|
|
57
|
+
open={showLightbox}
|
|
58
|
+
/>
|
|
59
|
+
</>
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export default FileList;
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { ActionIcon } from '@lobehub/ui';
|
|
2
2
|
import { Popconfirm } from 'antd';
|
|
3
3
|
import { Eraser } from 'lucide-react';
|
|
4
|
-
import { memo } from 'react';
|
|
4
|
+
import { memo, useCallback } from 'react';
|
|
5
5
|
import { useHotkeys } from 'react-hotkeys-hook';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
|
|
8
8
|
import HotKeys from '@/components/HotKeys';
|
|
9
9
|
import { CLEAN_MESSAGE_KEY, PREFIX_KEY } from '@/const/hotkeys';
|
|
10
|
+
import { useFileStore } from '@/store/files';
|
|
10
11
|
import { useSessionStore } from '@/store/session';
|
|
11
12
|
|
|
12
13
|
const Clear = memo(() => {
|
|
13
14
|
const { t } = useTranslation('setting');
|
|
14
|
-
const [clearMessage] = useSessionStore((s) => [s.clearMessage
|
|
15
|
+
const [clearMessage] = useSessionStore((s) => [s.clearMessage]);
|
|
16
|
+
const [clearImageList] = useFileStore((s) => [s.clearImageList]);
|
|
15
17
|
const hotkeys = [PREFIX_KEY, CLEAN_MESSAGE_KEY].join('+');
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
const resetConversation = useCallback(() => {
|
|
20
|
+
clearMessage();
|
|
21
|
+
clearImageList();
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
useHotkeys(hotkeys, resetConversation, {
|
|
18
25
|
preventDefault: true,
|
|
19
26
|
});
|
|
20
27
|
|
|
@@ -23,7 +30,9 @@ const Clear = memo(() => {
|
|
|
23
30
|
cancelText={t('cancel', { ns: 'common' })}
|
|
24
31
|
okButtonProps={{ danger: true }}
|
|
25
32
|
okText={t('ok', { ns: 'common' })}
|
|
26
|
-
onConfirm={() =>
|
|
33
|
+
onConfirm={() => {
|
|
34
|
+
resetConversation();
|
|
35
|
+
}}
|
|
27
36
|
placement={'topRight'}
|
|
28
37
|
title={t('confirmClearCurrentMessages', { ns: 'chat' })}
|
|
29
38
|
>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ActionIcon, Icon } from '@lobehub/ui';
|
|
2
|
+
import { Upload } from 'antd';
|
|
3
|
+
import { useTheme } from 'antd-style';
|
|
4
|
+
import { LucideImage, LucideLoader2 } from 'lucide-react';
|
|
5
|
+
import { memo, useState } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { Center } from 'react-layout-kit';
|
|
8
|
+
|
|
9
|
+
import { useFileStore } from '@/store/files';
|
|
10
|
+
import { useSessionStore } from '@/store/session';
|
|
11
|
+
import { agentSelectors } from '@/store/session/slices/agentConfig';
|
|
12
|
+
|
|
13
|
+
const FileUpload = memo(() => {
|
|
14
|
+
const { t } = useTranslation('chat');
|
|
15
|
+
const [loading, setLoading] = useState(false);
|
|
16
|
+
const theme = useTheme();
|
|
17
|
+
const upload = useFileStore((s) => s.uploadFile);
|
|
18
|
+
|
|
19
|
+
const canUpload = useSessionStore(agentSelectors.modelHasVisionAbility);
|
|
20
|
+
|
|
21
|
+
if (!canUpload) return null;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Upload
|
|
25
|
+
accept="image/*"
|
|
26
|
+
beforeUpload={async (file) => {
|
|
27
|
+
setLoading(true);
|
|
28
|
+
|
|
29
|
+
await upload(file);
|
|
30
|
+
|
|
31
|
+
setLoading(false);
|
|
32
|
+
return false;
|
|
33
|
+
}}
|
|
34
|
+
multiple={true}
|
|
35
|
+
showUploadList={false}
|
|
36
|
+
>
|
|
37
|
+
{loading ? (
|
|
38
|
+
<Center height={36} width={36}>
|
|
39
|
+
<Icon
|
|
40
|
+
color={theme.colorTextSecondary}
|
|
41
|
+
icon={LucideLoader2}
|
|
42
|
+
size={{ fontSize: 18 }}
|
|
43
|
+
spin
|
|
44
|
+
></Icon>
|
|
45
|
+
</Center>
|
|
46
|
+
) : (
|
|
47
|
+
<ActionIcon icon={LucideImage} placement={'bottom'} title={t('upload.actionTooltip')} />
|
|
48
|
+
)}
|
|
49
|
+
</Upload>
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export default FileUpload;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { FC } from 'react';
|
|
2
2
|
|
|
3
3
|
import Clear from './Clear';
|
|
4
|
+
import FileUpload from './FileUpload';
|
|
4
5
|
import History from './History';
|
|
5
6
|
import ModelSwitch from './ModelSwitch';
|
|
6
7
|
import Temperature from './Temperature';
|
|
@@ -8,6 +9,7 @@ import Token from './Token';
|
|
|
8
9
|
|
|
9
10
|
export const actionMap: Record<string, FC> = {
|
|
10
11
|
clear: Clear,
|
|
12
|
+
fileUpload: FileUpload,
|
|
11
13
|
history: History,
|
|
12
14
|
model: ModelSwitch,
|
|
13
15
|
temperature: Temperature,
|
|
@@ -15,5 +17,5 @@ export const actionMap: Record<string, FC> = {
|
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
// we can make these action lists configurable in the future
|
|
18
|
-
export const leftActionList = ['model', 'temperature', 'history', 'token'];
|
|
20
|
+
export const leftActionList = ['model', 'fileUpload', 'temperature', 'history', 'token'];
|
|
19
21
|
export const rightActionList = ['clear'];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
|
|
3
|
+
import { filesSelectors, useFileStore } from '@/store/files';
|
|
3
4
|
import { useSessionStore } from '@/store/session';
|
|
4
5
|
|
|
5
6
|
export const useSendMessage = () => {
|
|
@@ -11,7 +12,10 @@ export const useSendMessage = () => {
|
|
|
11
12
|
return useCallback(() => {
|
|
12
13
|
const store = useSessionStore.getState();
|
|
13
14
|
if (!!store.chatLoadingId) return;
|
|
14
|
-
|
|
15
|
+
const imageList = filesSelectors.imageUrlOrBase64List(useFileStore.getState());
|
|
16
|
+
|
|
17
|
+
sendMessage(store.inputMessage, imageList);
|
|
15
18
|
updateInputMessage('');
|
|
19
|
+
useFileStore.getState().clearImageList();
|
|
16
20
|
}, []);
|
|
17
21
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ReactNode, memo } from 'react';
|
|
2
|
+
import { Flexbox } from 'react-layout-kit';
|
|
3
|
+
|
|
4
|
+
import FileList from '@/app/chat/components/FileList';
|
|
5
|
+
import { LOADING_FLAT } from '@/const/message';
|
|
6
|
+
import { ChatMessage } from '@/types/chatMessage';
|
|
7
|
+
|
|
8
|
+
import BubblesLoading from '../Loading';
|
|
9
|
+
|
|
10
|
+
export const UserMessage = memo<
|
|
11
|
+
ChatMessage & {
|
|
12
|
+
editableContent: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
>(({ id, editableContent, content, ...res }) => {
|
|
15
|
+
if (content === LOADING_FLAT) return <BubblesLoading />;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Flexbox gap={8} id={id}>
|
|
19
|
+
{editableContent}
|
|
20
|
+
{res.files && <FileList editable={false} items={res.files} />}
|
|
21
|
+
</Flexbox>
|
|
22
|
+
);
|
|
23
|
+
});
|
|
@@ -10,11 +10,13 @@ import { pathString } from '@/utils/url';
|
|
|
10
10
|
import { AssistantMessage } from './Assistant';
|
|
11
11
|
import { DefaultMessage } from './Default';
|
|
12
12
|
import { FunctionMessage } from './Function';
|
|
13
|
+
import { UserMessage } from './User';
|
|
13
14
|
|
|
14
15
|
export const renderMessages: ChatListProps['renderMessages'] = {
|
|
15
16
|
assistant: AssistantMessage,
|
|
16
17
|
default: DefaultMessage,
|
|
17
18
|
function: FunctionMessage,
|
|
19
|
+
user: UserMessage,
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
export const useAvatarsClick = (): ChatListProps['onAvatarsClick'] => {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { getClientConfig } from '../client';
|
|
4
|
+
|
|
5
|
+
// 测试前重置 process.env
|
|
6
|
+
vi.stubGlobal('process', {
|
|
7
|
+
...process, // 保持原有的 process 对象
|
|
8
|
+
env: { ...process.env }, // 克隆环境变量对象,以便修改
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('getClientConfig', () => {
|
|
12
|
+
it('should return default URLs when no environment variables are set', () => {
|
|
13
|
+
const config = getClientConfig();
|
|
14
|
+
expect(config.AGENTS_INDEX_URL).toBe('https://chat-agents.lobehub.com');
|
|
15
|
+
expect(config.PLUGINS_INDEX_URL).toBe('https://chat-plugins.lobehub.com');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should return custom URLs when environment variables are set', () => {
|
|
19
|
+
process.env.AGENTS_INDEX_URL = 'https://custom-agents-url.com';
|
|
20
|
+
process.env.PLUGINS_INDEX_URL = 'https://custom-plugins-url.com';
|
|
21
|
+
const config = getClientConfig();
|
|
22
|
+
expect(config.AGENTS_INDEX_URL).toBe('https://custom-agents-url.com');
|
|
23
|
+
expect(config.PLUGINS_INDEX_URL).toBe('https://custom-plugins-url.com');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should return default URLs when environment variables are empty string', () => {
|
|
27
|
+
process.env.AGENTS_INDEX_URL = '';
|
|
28
|
+
process.env.PLUGINS_INDEX_URL = '';
|
|
29
|
+
|
|
30
|
+
const config = getClientConfig();
|
|
31
|
+
expect(config.AGENTS_INDEX_URL).toBe('https://chat-agents.lobehub.com');
|
|
32
|
+
expect(config.PLUGINS_INDEX_URL).toBe('https://chat-plugins.lobehub.com');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should correctly reflect boolean values for analytics flags', () => {
|
|
36
|
+
process.env.NEXT_PUBLIC_ANALYTICS_VERCEL = '1';
|
|
37
|
+
process.env.NEXT_PUBLIC_VERCEL_DEBUG = '1';
|
|
38
|
+
process.env.NEXT_PUBLIC_ANALYTICS_PLAUSIBLE = '1';
|
|
39
|
+
process.env.NEXT_PUBLIC_ANALYTICS_POSTHOG = '1';
|
|
40
|
+
process.env.NEXT_PUBLIC_POSTHOG_DEBUG = '1';
|
|
41
|
+
process.env.NEXT_PUBLIC_I18N_DEBUG = '1';
|
|
42
|
+
process.env.NEXT_PUBLIC_I18N_DEBUG_BROWSER = '1';
|
|
43
|
+
process.env.NEXT_PUBLIC_I18N_DEBUG_SERVER = '1';
|
|
44
|
+
|
|
45
|
+
const config = getClientConfig();
|
|
46
|
+
expect(config.ANALYTICS_VERCEL).toBe(true);
|
|
47
|
+
expect(config.VERCEL_DEBUG).toBe(true);
|
|
48
|
+
expect(config.ANALYTICS_PLAUSIBLE).toBe(true);
|
|
49
|
+
expect(config.ANALYTICS_POSTHOG).toBe(true);
|
|
50
|
+
expect(config.POSTHOG_DEBUG).toBe(true);
|
|
51
|
+
expect(config.I18N_DEBUG).toBe(true);
|
|
52
|
+
expect(config.I18N_DEBUG_BROWSER).toBe(true);
|
|
53
|
+
expect(config.I18N_DEBUG_SERVER).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should correctly handle falsy values for analytics flags', () => {
|
|
57
|
+
process.env.NEXT_PUBLIC_ANALYTICS_VERCEL = '0';
|
|
58
|
+
process.env.NEXT_PUBLIC_VERCEL_DEBUG = '0';
|
|
59
|
+
process.env.NEXT_PUBLIC_ANALYTICS_PLAUSIBLE = '0';
|
|
60
|
+
process.env.NEXT_PUBLIC_ANALYTICS_POSTHOG = '0';
|
|
61
|
+
process.env.NEXT_PUBLIC_POSTHOG_DEBUG = '0';
|
|
62
|
+
process.env.NEXT_PUBLIC_I18N_DEBUG = '0';
|
|
63
|
+
process.env.NEXT_PUBLIC_I18N_DEBUG_BROWSER = '0';
|
|
64
|
+
process.env.NEXT_PUBLIC_I18N_DEBUG_SERVER = '0';
|
|
65
|
+
|
|
66
|
+
const config = getClientConfig();
|
|
67
|
+
expect(config.ANALYTICS_VERCEL).toBe(false);
|
|
68
|
+
expect(config.VERCEL_DEBUG).toBe(false);
|
|
69
|
+
expect(config.ANALYTICS_PLAUSIBLE).toBe(false);
|
|
70
|
+
expect(config.ANALYTICS_POSTHOG).toBe(false);
|
|
71
|
+
expect(config.POSTHOG_DEBUG).toBe(false);
|
|
72
|
+
expect(config.I18N_DEBUG).toBe(false);
|
|
73
|
+
expect(config.I18N_DEBUG_BROWSER).toBe(false);
|
|
74
|
+
expect(config.I18N_DEBUG_SERVER).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { getServerConfig } from '../server';
|
|
4
|
+
|
|
5
|
+
// Stub the global process object to safely mock environment variables
|
|
6
|
+
vi.stubGlobal('process', {
|
|
7
|
+
...process, // Preserve the original process object
|
|
8
|
+
env: { ...process.env }, // Clone the environment variables object for modification
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('getServerConfig', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Reset environment variables before each test case
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('throws an error if process is undefined', () => {
|
|
18
|
+
const originalProcess = global.process;
|
|
19
|
+
// To simulate the error condition, temporarily set the global process to `undefined`,
|
|
20
|
+
// @ts-ignore
|
|
21
|
+
global.process = undefined;
|
|
22
|
+
|
|
23
|
+
expect(() => getServerConfig()).toThrow(
|
|
24
|
+
'[Server Config] you are importing a nodejs-only module outside of nodejs',
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
global.process = originalProcess; // Restore the original process object
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('correctly reflects boolean value for USE_AZURE_OPENAI', () => {
|
|
31
|
+
process.env.USE_AZURE_OPENAI = '1';
|
|
32
|
+
const config = getServerConfig();
|
|
33
|
+
expect(config.USE_AZURE_OPENAI).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('correctly handles falsy values for USE_AZURE_OPENAI', () => {
|
|
37
|
+
process.env.USE_AZURE_OPENAI = '0';
|
|
38
|
+
const config = getServerConfig();
|
|
39
|
+
expect(config.USE_AZURE_OPENAI).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('returns default IMGUR_CLIENT_ID when no environment variable is set', () => {
|
|
43
|
+
const config = getServerConfig();
|
|
44
|
+
expect(config.IMGUR_CLIENT_ID).toBe('e415f320d6e24f9');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('returns custom IMGUR_CLIENT_ID when environment variable is set', () => {
|
|
48
|
+
process.env.IMGUR_CLIENT_ID = 'custom-client-id';
|
|
49
|
+
const config = getServerConfig();
|
|
50
|
+
expect(config.IMGUR_CLIENT_ID).toBe('custom-client-id');
|
|
51
|
+
});
|
|
52
|
+
});
|
package/src/config/server.ts
CHANGED
|
@@ -12,10 +12,16 @@ declare global {
|
|
|
12
12
|
AZURE_API_KEY?: string;
|
|
13
13
|
AZURE_API_VERSION?: string;
|
|
14
14
|
USE_AZURE_OPENAI?: string;
|
|
15
|
+
|
|
16
|
+
IMGUR_CLIENT_ID?: string;
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
// we apply a free imgur app to get a client id
|
|
22
|
+
// refs: https://apidocs.imgur.com/
|
|
23
|
+
const DEFAULT_IMAGUR_CLIENT_ID = 'e415f320d6e24f9';
|
|
24
|
+
|
|
19
25
|
export const getServerConfig = () => {
|
|
20
26
|
if (typeof process === 'undefined') {
|
|
21
27
|
throw new Error('[Server Config] you are importing a nodejs-only module outside of nodejs');
|
|
@@ -30,5 +36,7 @@ export const getServerConfig = () => {
|
|
|
30
36
|
AZURE_API_KEY: process.env.AZURE_API_KEY,
|
|
31
37
|
AZURE_API_VERSION: process.env.AZURE_API_VERSION,
|
|
32
38
|
USE_AZURE_OPENAI: process.env.USE_AZURE_OPENAI === '1',
|
|
39
|
+
|
|
40
|
+
IMGUR_CLIENT_ID: process.env.IMGUR_CLIENT_ID || DEFAULT_IMAGUR_CLIENT_ID,
|
|
33
41
|
};
|
|
34
42
|
};
|
|
@@ -3,7 +3,7 @@ import type { FormProps } from '@lobehub/ui';
|
|
|
3
3
|
export const HEADER_HEIGHT = 64;
|
|
4
4
|
export const MOBILE_NABBAR_HEIGHT = 44;
|
|
5
5
|
export const MOBILE_TABBAR_HEIGHT = 48;
|
|
6
|
-
export const CHAT_TEXTAREA_HEIGHT =
|
|
6
|
+
export const CHAT_TEXTAREA_HEIGHT = 230;
|
|
7
7
|
export const CHAT_TEXTAREA_HEIGHT_MOBILE = 108;
|
|
8
8
|
export const CHAT_SIDEBAR_WIDTH = 280;
|
|
9
9
|
export const MARKET_SIDEBAR_WIDTH = 400;
|
package/src/const/llm.ts
CHANGED
|
@@ -12,3 +12,6 @@ export const LanguageModelWhiteList = [
|
|
|
12
12
|
];
|
|
13
13
|
|
|
14
14
|
export const DEFAULT_OPENAI_MODEL_LIST: string[] = Object.values(LanguageModel);
|
|
15
|
+
|
|
16
|
+
// vision model white list, these models will change the content from string to array
|
|
17
|
+
export const VISION_MODEL_WHITE_LIST = ['gpt-4-vision-preview'];
|
package/src/const/settings.ts
CHANGED