@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.
Files changed (83) hide show
  1. package/.eslintrc.js +1 -0
  2. package/CHANGELOG.md +50 -0
  3. package/README.md +2 -2
  4. package/README.zh-CN.md +2 -2
  5. package/__mocks__/zustand/traditional.ts +25 -0
  6. package/package.json +7 -3
  7. package/src/app/api/files/image/imgur.ts +72 -0
  8. package/src/app/api/files/image/route.ts +42 -0
  9. package/src/app/api/openai/chat/createChatCompletion.ts +2 -1
  10. package/src/app/chat/(desktop)/features/ChatInput/DragUpload.tsx +166 -0
  11. package/src/app/chat/(desktop)/features/ChatInput/Footer/LocalFiles.tsx +10 -0
  12. package/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +64 -0
  13. package/src/app/chat/(desktop)/features/ChatInput/InputArea.tsx +69 -0
  14. package/src/app/chat/(desktop)/features/ChatInput/index.tsx +60 -19
  15. package/src/app/chat/(mobile)/features/ChatInput/Mobile.tsx +79 -0
  16. package/src/app/chat/(mobile)/features/{ChatInput.tsx → ChatInput/index.tsx} +19 -3
  17. package/src/app/chat/(mobile)/features/ChatInput/style.mobile.ts +19 -0
  18. package/src/app/chat/components/FileList/FileItem.style.ts +81 -0
  19. package/src/app/chat/components/FileList/FileItem.tsx +69 -0
  20. package/src/app/chat/components/FileList/Lightbox.tsx +46 -0
  21. package/src/app/chat/components/FileList/index.tsx +63 -0
  22. package/src/app/chat/features/ChatInput/ActionBar/Clear.tsx +48 -0
  23. package/src/app/chat/features/ChatInput/ActionBar/FileUpload.tsx +53 -0
  24. package/src/app/chat/features/ChatInput/ActionBar/History.tsx +64 -0
  25. package/src/app/chat/features/ChatInput/ActionBar/ModelSwitch.tsx +42 -0
  26. package/src/app/chat/features/ChatInput/ActionBar/Temperature.tsx +47 -0
  27. package/src/app/chat/features/{ChatInputContent/ActionBar/Token.tsx → ChatInput/ActionBar/Token/TokenTag.tsx} +4 -4
  28. package/src/app/chat/features/ChatInput/ActionBar/Token/index.tsx +21 -0
  29. package/src/app/chat/features/ChatInput/ActionBar/config.ts +21 -0
  30. package/src/app/chat/features/ChatInput/ActionBar/index.tsx +35 -0
  31. package/src/app/chat/features/ChatInput/useSend.ts +21 -0
  32. package/src/app/chat/features/Conversation/ChatList/Messages/User.tsx +23 -0
  33. package/src/app/chat/features/Conversation/ChatList/Messages/index.ts +2 -0
  34. package/src/config/__tests__/client.test.ts +76 -0
  35. package/src/config/__tests__/server.test.ts +52 -0
  36. package/src/config/server.ts +8 -0
  37. package/src/const/layoutTokens.ts +1 -1
  38. package/src/const/llm.ts +3 -0
  39. package/src/const/settings.ts +2 -0
  40. package/src/database/core/__tests__/db.test.ts +77 -0
  41. package/src/database/core/__tests__/model.test.ts +55 -0
  42. package/src/database/core/db.ts +25 -0
  43. package/src/database/core/index.ts +1 -0
  44. package/src/database/core/model.ts +49 -0
  45. package/src/database/core/schema.ts +9 -0
  46. package/src/database/models/file.test.ts +71 -0
  47. package/src/database/models/file.ts +26 -0
  48. package/src/locales/default/chat.ts +5 -0
  49. package/src/services/__tests__/file.test.ts +88 -0
  50. package/src/services/chatModel.ts +11 -6
  51. package/src/services/file.ts +36 -0
  52. package/src/store/files/index.ts +2 -0
  53. package/src/store/files/initialState.ts +7 -0
  54. package/src/store/files/selectors.test.ts +75 -0
  55. package/src/store/files/selectors.ts +34 -0
  56. package/src/store/files/slices/images/action.test.ts +186 -0
  57. package/src/store/files/slices/images/action.ts +83 -0
  58. package/src/store/files/slices/images/index.ts +3 -0
  59. package/src/store/files/slices/images/initialState.ts +11 -0
  60. package/src/store/files/store.ts +27 -0
  61. package/src/store/global/selectors/settings.test.ts +1 -3
  62. package/src/store/session/slices/agentConfig/selectors.ts +7 -1
  63. package/src/store/session/slices/chat/actions/message.ts +83 -18
  64. package/src/store/session/slices/chat/initialState.ts +4 -1
  65. package/src/store/session/slices/chat/reducers/files.test.ts +38 -0
  66. package/src/store/session/slices/chat/reducers/files.ts +37 -0
  67. package/src/store/session/slices/chat/selectors/chat.ts +3 -2
  68. package/src/store/session/slices/session/initialState.ts +1 -0
  69. package/src/store/session/slices/session/reducers/session.test.ts +98 -1
  70. package/src/store/session/slices/session/reducers/session.ts +26 -1
  71. package/src/types/chatMessage.ts +3 -2
  72. package/src/types/database/db.ts +4 -0
  73. package/src/types/database/files.ts +38 -0
  74. package/src/types/files.ts +8 -0
  75. package/src/types/openai/chat.ts +15 -1
  76. package/src/types/session.ts +1 -0
  77. package/tests/setup.ts +8 -0
  78. package/vitest.config.ts +2 -0
  79. package/src/app/chat/(desktop)/features/ChatInput/Footer.tsx +0 -35
  80. package/src/app/chat/features/ChatInputContent/ActionBar/ActionLeft.tsx +0 -117
  81. package/src/app/chat/features/ChatInputContent/ActionBar/ActionRight.tsx +0 -48
  82. package/src/app/chat/features/ChatInputContent/index.tsx +0 -71
  83. /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 ChatInputContent from '../../../features/ChatInputContent';
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
- <DraggablePanel
19
- fullscreen={expand}
20
- headerHeight={HEADER_HEIGHT}
21
- minHeight={CHAT_TEXTAREA_HEIGHT}
22
- onSizeChange={(_, size) => {
23
- if (!size) return;
24
-
25
- updatePreference({
26
- inputHeight: typeof size.height === 'string' ? Number.parseInt(size.height) : size.height,
27
- });
28
- }}
29
- placement="bottom"
30
- size={{ height: inputHeight, width: '100%' }}
31
- style={{ zIndex: 10 }}
32
- >
33
- <ChatInputContent expand={expand} footer={<Footer />} onExpandChange={setExpand} />
34
- </DraggablePanel>
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 ChatInputContent from '../../features/ChatInputContent';
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
- <ChatInputContent mobile />
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;