@lobehub/chat 0.100.4 → 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 +50 -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 +69 -0
- package/src/app/chat/(desktop)/features/ChatInput/index.tsx +60 -19
- package/src/app/chat/(mobile)/features/ChatInput/Mobile.tsx +79 -0
- package/src/app/chat/(mobile)/features/{ChatInput.tsx → ChatInput/index.tsx} +19 -3
- package/src/app/chat/(mobile)/features/ChatInput/style.mobile.ts +19 -0
- 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 +48 -0
- package/src/app/chat/features/ChatInput/ActionBar/FileUpload.tsx +53 -0
- package/src/app/chat/features/ChatInput/ActionBar/History.tsx +64 -0
- package/src/app/chat/features/ChatInput/ActionBar/ModelSwitch.tsx +42 -0
- package/src/app/chat/features/ChatInput/ActionBar/Temperature.tsx +47 -0
- package/src/app/chat/features/{ChatInputContent/ActionBar/Token.tsx → ChatInput/ActionBar/Token/TokenTag.tsx} +4 -4
- package/src/app/chat/features/ChatInput/ActionBar/Token/index.tsx +21 -0
- package/src/app/chat/features/ChatInput/ActionBar/config.ts +21 -0
- package/src/app/chat/features/ChatInput/ActionBar/index.tsx +35 -0
- package/src/app/chat/features/ChatInput/useSend.ts +21 -0
- 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 +83 -18
- package/src/store/session/slices/chat/initialState.ts +4 -1
- 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/chat/selectors/chat.ts +3 -2
- 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 -35
- package/src/app/chat/features/ChatInputContent/ActionBar/ActionLeft.tsx +0 -117
- package/src/app/chat/features/ChatInputContent/ActionBar/ActionRight.tsx +0 -48
- package/src/app/chat/features/ChatInputContent/index.tsx +0 -71
- /package/src/app/chat/features/{ChatInputContent → ChatInput}/Topic/index.tsx +0 -0
|
@@ -1,37 +1,78 @@
|
|
|
1
|
-
import { DraggablePanel } from '@lobehub/ui';
|
|
1
|
+
import { ActionIcon, DraggablePanel } from '@lobehub/ui';
|
|
2
|
+
import { createStyles } from 'antd-style';
|
|
3
|
+
import { Maximize2, Minimize2 } from 'lucide-react';
|
|
2
4
|
import { memo, useState } from 'react';
|
|
3
5
|
|
|
6
|
+
import ActionBar from '@/app/chat/features/ChatInput/ActionBar';
|
|
4
7
|
import { CHAT_TEXTAREA_HEIGHT, HEADER_HEIGHT } from '@/const/layoutTokens';
|
|
5
8
|
import { useGlobalStore } from '@/store/global';
|
|
9
|
+
import { useSessionStore } from '@/store/session';
|
|
10
|
+
import { agentSelectors } from '@/store/session/selectors';
|
|
6
11
|
|
|
7
|
-
import
|
|
12
|
+
import DragUpload from './DragUpload';
|
|
8
13
|
import Footer from './Footer';
|
|
14
|
+
import InputArea from './InputArea';
|
|
15
|
+
|
|
16
|
+
const useStyles = createStyles(({ css }) => {
|
|
17
|
+
return {
|
|
18
|
+
container: css`
|
|
19
|
+
position: relative;
|
|
20
|
+
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
gap: 8px;
|
|
24
|
+
|
|
25
|
+
height: 100%;
|
|
26
|
+
padding: 12px 0 16px;
|
|
27
|
+
`,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
9
30
|
|
|
10
31
|
const ChatInputDesktopLayout = memo(() => {
|
|
32
|
+
const { styles } = useStyles();
|
|
11
33
|
const [expand, setExpand] = useState<boolean>(false);
|
|
34
|
+
|
|
12
35
|
const [inputHeight, updatePreference] = useGlobalStore((s) => [
|
|
13
36
|
s.preference.inputHeight,
|
|
14
37
|
s.updatePreference,
|
|
15
38
|
]);
|
|
16
39
|
|
|
40
|
+
const canUpload = useSessionStore(agentSelectors.modelHasVisionAbility);
|
|
17
41
|
return (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
<>
|
|
43
|
+
{canUpload && <DragUpload />}
|
|
44
|
+
<DraggablePanel
|
|
45
|
+
fullscreen={expand}
|
|
46
|
+
headerHeight={HEADER_HEIGHT}
|
|
47
|
+
minHeight={CHAT_TEXTAREA_HEIGHT}
|
|
48
|
+
onSizeChange={(_, size) => {
|
|
49
|
+
if (!size) return;
|
|
50
|
+
|
|
51
|
+
updatePreference({
|
|
52
|
+
inputHeight:
|
|
53
|
+
typeof size.height === 'string' ? Number.parseInt(size.height) : size.height,
|
|
54
|
+
});
|
|
55
|
+
}}
|
|
56
|
+
placement="bottom"
|
|
57
|
+
size={{ height: inputHeight, width: '100%' }}
|
|
58
|
+
style={{ zIndex: 10 }}
|
|
59
|
+
>
|
|
60
|
+
<section className={styles.container} style={{ minHeight: CHAT_TEXTAREA_HEIGHT }}>
|
|
61
|
+
<ActionBar
|
|
62
|
+
rightAreaEndRender={
|
|
63
|
+
<ActionIcon
|
|
64
|
+
icon={expand ? Minimize2 : Maximize2}
|
|
65
|
+
onClick={() => {
|
|
66
|
+
setExpand(!expand);
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
}
|
|
70
|
+
/>
|
|
71
|
+
<InputArea />
|
|
72
|
+
<Footer />
|
|
73
|
+
</section>
|
|
74
|
+
</DraggablePanel>
|
|
75
|
+
</>
|
|
35
76
|
);
|
|
36
77
|
});
|
|
37
78
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Icon, Input } from '@lobehub/ui';
|
|
2
|
+
import { Button, type InputRef } from 'antd';
|
|
3
|
+
import { Loader2, SendHorizonal } from 'lucide-react';
|
|
4
|
+
import { forwardRef, useCallback, useRef } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { Flexbox } from 'react-layout-kit';
|
|
7
|
+
import useControlledState from 'use-merge-value';
|
|
8
|
+
|
|
9
|
+
import ActionBar from '@/app/chat/features/ChatInput/ActionBar';
|
|
10
|
+
import SaveTopic from '@/app/chat/features/ChatInput/Topic';
|
|
11
|
+
|
|
12
|
+
import { useStyles } from './style.mobile';
|
|
13
|
+
|
|
14
|
+
export type ChatInputAreaMobile = {
|
|
15
|
+
loading?: boolean;
|
|
16
|
+
onChange?: (value: string) => void;
|
|
17
|
+
onSend?: (value: string) => void;
|
|
18
|
+
onStop?: () => void;
|
|
19
|
+
value?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const ChatInputArea = forwardRef<InputRef, ChatInputAreaMobile>(
|
|
23
|
+
({ onSend, loading, onChange, onStop, value }) => {
|
|
24
|
+
const { t } = useTranslation('chat');
|
|
25
|
+
const [currentValue, setCurrentValue] = useControlledState<string>('', {
|
|
26
|
+
onChange: onChange,
|
|
27
|
+
value,
|
|
28
|
+
});
|
|
29
|
+
const { cx, styles } = useStyles();
|
|
30
|
+
const isChineseInput = useRef(false);
|
|
31
|
+
|
|
32
|
+
const handleSend = useCallback(() => {
|
|
33
|
+
if (loading) return;
|
|
34
|
+
if (onSend) onSend(currentValue);
|
|
35
|
+
setCurrentValue('');
|
|
36
|
+
}, [currentValue]);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Flexbox className={cx(styles.container)} gap={12}>
|
|
40
|
+
<ActionBar rightAreaStartRender={<SaveTopic />} />
|
|
41
|
+
<Flexbox className={styles.inner} gap={8} horizontal>
|
|
42
|
+
<Input
|
|
43
|
+
className={cx(styles.input)}
|
|
44
|
+
onBlur={(e) => {
|
|
45
|
+
setCurrentValue(e.target.value);
|
|
46
|
+
}}
|
|
47
|
+
onChange={(e) => {
|
|
48
|
+
setCurrentValue(e.target.value);
|
|
49
|
+
}}
|
|
50
|
+
onCompositionEnd={() => {
|
|
51
|
+
isChineseInput.current = false;
|
|
52
|
+
}}
|
|
53
|
+
onCompositionStart={() => {
|
|
54
|
+
isChineseInput.current = true;
|
|
55
|
+
}}
|
|
56
|
+
onPressEnter={(e) => {
|
|
57
|
+
if (!loading && !e.shiftKey && !isChineseInput.current) {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
handleSend();
|
|
60
|
+
}
|
|
61
|
+
}}
|
|
62
|
+
placeholder={t('sendPlaceholder')}
|
|
63
|
+
type={'block'}
|
|
64
|
+
value={currentValue}
|
|
65
|
+
/>
|
|
66
|
+
<div>
|
|
67
|
+
{loading ? (
|
|
68
|
+
<Button icon={loading && <Icon icon={Loader2} spin />} onClick={onStop} />
|
|
69
|
+
) : (
|
|
70
|
+
<Button icon={<Icon icon={SendHorizonal} />} onClick={handleSend} type={'primary'} />
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
</Flexbox>
|
|
74
|
+
</Flexbox>
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
export default ChatInputArea;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createStyles } from 'antd-style';
|
|
2
|
-
import { memo } from 'react';
|
|
2
|
+
import { memo, useState } from 'react';
|
|
3
3
|
|
|
4
4
|
import SafeSpacing from '@/components/SafeSpacing';
|
|
5
5
|
import { CHAT_TEXTAREA_HEIGHT_MOBILE } from '@/const/layoutTokens';
|
|
6
|
+
import { useSessionStore } from '@/store/session';
|
|
6
7
|
|
|
7
|
-
import
|
|
8
|
+
import ChatInputArea from './Mobile';
|
|
8
9
|
|
|
9
10
|
const useStyles = createStyles(
|
|
10
11
|
({ css, token }) => css`
|
|
@@ -21,11 +22,26 @@ const useStyles = createStyles(
|
|
|
21
22
|
|
|
22
23
|
const ChatInputMobileLayout = memo(() => {
|
|
23
24
|
const { styles } = useStyles();
|
|
25
|
+
|
|
26
|
+
const [message, setMessage] = useState('');
|
|
27
|
+
|
|
28
|
+
const [isLoading, sendMessage, stopGenerateMessage] = useSessionStore((s) => [
|
|
29
|
+
!!s.chatLoadingId,
|
|
30
|
+
s.sendMessage,
|
|
31
|
+
s.stopGenerateMessage,
|
|
32
|
+
]);
|
|
33
|
+
|
|
24
34
|
return (
|
|
25
35
|
<>
|
|
26
36
|
<SafeSpacing height={CHAT_TEXTAREA_HEIGHT_MOBILE} mobile position={'bottom'} />
|
|
27
37
|
<div className={styles}>
|
|
28
|
-
<
|
|
38
|
+
<ChatInputArea
|
|
39
|
+
loading={isLoading}
|
|
40
|
+
onChange={setMessage}
|
|
41
|
+
onSend={sendMessage}
|
|
42
|
+
onStop={stopGenerateMessage}
|
|
43
|
+
value={message}
|
|
44
|
+
/>
|
|
29
45
|
</div>
|
|
30
46
|
</>
|
|
31
47
|
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createStyles } from 'antd-style';
|
|
2
|
+
import { rgba } from 'polished';
|
|
3
|
+
|
|
4
|
+
export const useStyles = createStyles(({ css, token }) => {
|
|
5
|
+
return {
|
|
6
|
+
container: css`
|
|
7
|
+
padding: 12px 0;
|
|
8
|
+
background: ${token.colorBgLayout};
|
|
9
|
+
border-top: 1px solid ${rgba(token.colorBorder, 0.25)};
|
|
10
|
+
`,
|
|
11
|
+
inner: css`
|
|
12
|
+
padding: 0 16px;
|
|
13
|
+
`,
|
|
14
|
+
input: css`
|
|
15
|
+
background: ${token.colorFillSecondary} !important;
|
|
16
|
+
border: none !important;
|
|
17
|
+
`,
|
|
18
|
+
};
|
|
19
|
+
});
|
|
@@ -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;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ActionIcon } from '@lobehub/ui';
|
|
2
|
+
import { Popconfirm } from 'antd';
|
|
3
|
+
import { Eraser } from 'lucide-react';
|
|
4
|
+
import { memo, useCallback } from 'react';
|
|
5
|
+
import { useHotkeys } from 'react-hotkeys-hook';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
|
|
8
|
+
import HotKeys from '@/components/HotKeys';
|
|
9
|
+
import { CLEAN_MESSAGE_KEY, PREFIX_KEY } from '@/const/hotkeys';
|
|
10
|
+
import { useFileStore } from '@/store/files';
|
|
11
|
+
import { useSessionStore } from '@/store/session';
|
|
12
|
+
|
|
13
|
+
const Clear = memo(() => {
|
|
14
|
+
const { t } = useTranslation('setting');
|
|
15
|
+
const [clearMessage] = useSessionStore((s) => [s.clearMessage]);
|
|
16
|
+
const [clearImageList] = useFileStore((s) => [s.clearImageList]);
|
|
17
|
+
const hotkeys = [PREFIX_KEY, CLEAN_MESSAGE_KEY].join('+');
|
|
18
|
+
|
|
19
|
+
const resetConversation = useCallback(() => {
|
|
20
|
+
clearMessage();
|
|
21
|
+
clearImageList();
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
useHotkeys(hotkeys, resetConversation, {
|
|
25
|
+
preventDefault: true,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Popconfirm
|
|
30
|
+
cancelText={t('cancel', { ns: 'common' })}
|
|
31
|
+
okButtonProps={{ danger: true }}
|
|
32
|
+
okText={t('ok', { ns: 'common' })}
|
|
33
|
+
onConfirm={() => {
|
|
34
|
+
resetConversation();
|
|
35
|
+
}}
|
|
36
|
+
placement={'topRight'}
|
|
37
|
+
title={t('confirmClearCurrentMessages', { ns: 'chat' })}
|
|
38
|
+
>
|
|
39
|
+
<ActionIcon
|
|
40
|
+
icon={Eraser}
|
|
41
|
+
placement={'bottom'}
|
|
42
|
+
title={(<HotKeys desc={t('clearCurrentMessages', { ns: 'chat' })} keys={hotkeys} />) as any}
|
|
43
|
+
/>
|
|
44
|
+
</Popconfirm>
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export default Clear;
|
|
@@ -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;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ActionIcon, SliderWithInput } from '@lobehub/ui';
|
|
2
|
+
import { Popover, Switch } from 'antd';
|
|
3
|
+
import { Timer, TimerOff } from 'lucide-react';
|
|
4
|
+
import { memo } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { Flexbox } from 'react-layout-kit';
|
|
7
|
+
|
|
8
|
+
import { useSessionStore } from '@/store/session';
|
|
9
|
+
import { agentSelectors } from '@/store/session/selectors';
|
|
10
|
+
|
|
11
|
+
const History = memo(() => {
|
|
12
|
+
const { t } = useTranslation('setting');
|
|
13
|
+
|
|
14
|
+
const [historyCount, unlimited, updateAgentConfig] = useSessionStore((s) => {
|
|
15
|
+
const config = agentSelectors.currentAgentConfig(s);
|
|
16
|
+
return [config.historyCount, !config.enableHistoryCount, s.updateAgentConfig];
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Popover
|
|
21
|
+
arrow={false}
|
|
22
|
+
content={
|
|
23
|
+
<Flexbox align={'center'} gap={16} horizontal>
|
|
24
|
+
<SliderWithInput
|
|
25
|
+
disabled={unlimited}
|
|
26
|
+
max={30}
|
|
27
|
+
min={1}
|
|
28
|
+
onChange={(v) => {
|
|
29
|
+
updateAgentConfig({ historyCount: v });
|
|
30
|
+
}}
|
|
31
|
+
step={1}
|
|
32
|
+
style={{ width: 160 }}
|
|
33
|
+
value={historyCount}
|
|
34
|
+
/>
|
|
35
|
+
<Flexbox align={'center'} gap={4} horizontal>
|
|
36
|
+
<Switch
|
|
37
|
+
checked={unlimited}
|
|
38
|
+
onChange={(checked) => {
|
|
39
|
+
updateAgentConfig({ enableHistoryCount: !checked });
|
|
40
|
+
}}
|
|
41
|
+
size={'small'}
|
|
42
|
+
/>
|
|
43
|
+
{t('settingChat.enableHistoryCount.alias')}
|
|
44
|
+
</Flexbox>
|
|
45
|
+
</Flexbox>
|
|
46
|
+
}
|
|
47
|
+
placement={'top'}
|
|
48
|
+
trigger={'click'}
|
|
49
|
+
>
|
|
50
|
+
<ActionIcon
|
|
51
|
+
icon={unlimited ? TimerOff : Timer}
|
|
52
|
+
placement={'bottom'}
|
|
53
|
+
title={t(
|
|
54
|
+
unlimited
|
|
55
|
+
? 'settingChat.enableHistoryCount.unlimited'
|
|
56
|
+
: 'settingChat.enableHistoryCount.limited',
|
|
57
|
+
{ number: historyCount || 0 },
|
|
58
|
+
)}
|
|
59
|
+
/>
|
|
60
|
+
</Popover>
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export default History;
|