@lobehub/chat 0.100.3 → 0.100.5

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 (24) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/package.json +4 -4
  3. package/src/app/chat/(desktop)/features/ChatInput/Footer.tsx +34 -6
  4. package/src/app/chat/(desktop)/features/ChatInput/InputArea.tsx +69 -0
  5. package/src/app/chat/(desktop)/features/ChatInput/index.tsx +37 -4
  6. package/src/app/chat/(desktop)/features/ChatInput/useSend.ts +17 -0
  7. package/src/app/chat/(mobile)/features/ChatInput/Mobile.tsx +79 -0
  8. package/src/app/chat/(mobile)/features/{ChatInput.tsx → ChatInput/index.tsx} +19 -3
  9. package/src/app/chat/(mobile)/features/ChatInput/style.mobile.ts +19 -0
  10. package/src/app/chat/features/ChatInput/ActionBar/Clear.tsx +39 -0
  11. package/src/app/chat/features/ChatInput/ActionBar/History.tsx +64 -0
  12. package/src/app/chat/features/ChatInput/ActionBar/ModelSwitch.tsx +42 -0
  13. package/src/app/chat/features/ChatInput/ActionBar/Temperature.tsx +47 -0
  14. package/src/app/chat/features/{ChatInputContent/ActionBar/Token.tsx → ChatInput/ActionBar/Token/TokenTag.tsx} +4 -4
  15. package/src/app/chat/features/ChatInput/ActionBar/Token/index.tsx +21 -0
  16. package/src/app/chat/features/ChatInput/ActionBar/config.ts +19 -0
  17. package/src/app/chat/features/ChatInput/ActionBar/index.tsx +35 -0
  18. package/src/app/chat/features/{ChatInputContent → ChatInput}/Topic/index.tsx +1 -0
  19. package/src/store/session/slices/chat/actions/message.ts +7 -3
  20. package/src/store/session/slices/chat/initialState.ts +4 -1
  21. package/src/store/session/slices/chat/selectors/chat.ts +3 -2
  22. package/src/app/chat/features/ChatInputContent/ActionBar/ActionLeft.tsx +0 -117
  23. package/src/app/chat/features/ChatInputContent/ActionBar/ActionRight.tsx +0 -48
  24. package/src/app/chat/features/ChatInputContent/index.tsx +0 -71
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.100.5](https://github.com/lobehub/lobe-chat/compare/v0.100.4...v0.100.5)
6
+
7
+ <sup>Released on **2023-11-11**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **misc**: Refactor the input area to suit the files upload feature.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Code refactoring
19
+
20
+ - **misc**: Refactor the input area to suit the files upload feature, closes [#442](https://github.com/lobehub/lobe-chat/issues/442) ([57a61fd](https://github.com/lobehub/lobe-chat/commit/57a61fd))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ### [Version 0.100.4](https://github.com/lobehub/lobe-chat/compare/v0.100.3...v0.100.4)
31
+
32
+ <sup>Released on **2023-11-11**</sup>
33
+
34
+ #### 🐛 Bug Fixes
35
+
36
+ - **misc**: Hotkey disabled in form tags.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's fixed
44
+
45
+ - **misc**: Hotkey disabled in form tags ([165888f](https://github.com/lobehub/lobe-chat/commit/165888f))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 0.100.3](https://github.com/lobehub/lobe-chat/compare/v0.100.2...v0.100.3)
6
56
 
7
57
  <sup>Released on **2023-11-09**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.100.3",
3
+ "version": "0.100.5",
4
4
  "description": "An open-source, extensible (Function Calling), high-performance chatbot framework. It supports one-click free deployment of your private ChatGPT/LLM web application",
5
5
  "keywords": [
6
6
  "chatbot",
@@ -92,7 +92,7 @@
92
92
  "lucide-react": "latest",
93
93
  "modern-screenshot": "^4",
94
94
  "nanoid": "^5",
95
- "next": "^14.0.1",
95
+ "next": "14.0.1",
96
96
  "openai": "~4.15.0",
97
97
  "polished": "^4",
98
98
  "posthog-js": "^1",
@@ -124,8 +124,8 @@
124
124
  "@ducanh2912/next-pwa": "^9.7.2",
125
125
  "@lobehub/i18n-cli": "latest",
126
126
  "@lobehub/lint": "latest",
127
- "@next/bundle-analyzer": "^14.0.1",
128
- "@next/eslint-plugin-next": "^14.0.1",
127
+ "@next/bundle-analyzer": "14.0.1",
128
+ "@next/eslint-plugin-next": "14.0.1",
129
129
  "@testing-library/jest-dom": "^6",
130
130
  "@testing-library/react": "^14",
131
131
  "@types/chroma-js": "^2",
@@ -1,18 +1,37 @@
1
1
  import { Icon } from '@lobehub/ui';
2
- import { useTheme } from 'antd-style';
3
- import { ArrowBigUp, CornerDownLeft } from 'lucide-react';
2
+ import { Button } from 'antd';
3
+ import { createStyles } from 'antd-style';
4
+ import { ArrowBigUp, CornerDownLeft, Loader2 } from 'lucide-react';
4
5
  import { memo } from 'react';
5
6
  import { useTranslation } from 'react-i18next';
6
7
  import { Flexbox } from 'react-layout-kit';
7
8
 
8
- import SaveTopic from '../../../features/ChatInputContent/Topic';
9
+ import SaveTopic from '@/app/chat/features/ChatInput/Topic';
10
+ import { useSessionStore } from '@/store/session';
11
+
12
+ import { useSendMessage } from './useSend';
13
+
14
+ const useStyles = createStyles(({ css }) => ({
15
+ footerBar: css`
16
+ display: flex;
17
+ flex: none;
18
+ gap: 8px;
19
+ align-items: center;
20
+ justify-content: flex-end;
21
+
22
+ padding: 0 24px;
23
+ `,
24
+ }));
9
25
 
10
26
  const Footer = memo(() => {
11
- const theme = useTheme();
12
27
  const { t } = useTranslation('chat');
28
+ const { styles, theme } = useStyles();
29
+ const [loading, onStop] = useSessionStore((s) => [!!s.chatLoadingId, s.stopGenerateMessage]);
30
+
31
+ const onSend = useSendMessage();
13
32
 
14
33
  return (
15
- <>
34
+ <div className={styles.footerBar}>
16
35
  <Flexbox
17
36
  gap={4}
18
37
  horizontal
@@ -28,7 +47,16 @@ const Footer = memo(() => {
28
47
  <span>{t('warp')}</span>
29
48
  </Flexbox>
30
49
  <SaveTopic />
31
- </>
50
+ {loading ? (
51
+ <Button icon={loading && <Icon icon={Loader2} spin />} onClick={onStop}>
52
+ {t('stop')}
53
+ </Button>
54
+ ) : (
55
+ <Button onClick={() => onSend()} type={'primary'}>
56
+ {t('send')}
57
+ </Button>
58
+ )}
59
+ </div>
32
60
  );
33
61
  });
34
62
 
@@ -0,0 +1,69 @@
1
+ import { TextArea } from '@lobehub/ui';
2
+ import { createStyles } from 'antd-style';
3
+ import { memo, useRef } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ import { useSessionStore } from '@/store/session';
7
+
8
+ import { useSendMessage } from './useSend';
9
+
10
+ const useStyles = createStyles(({ css }) => {
11
+ return {
12
+ textarea: css`
13
+ height: 100% !important;
14
+ padding: 0 24px;
15
+ line-height: 1.5;
16
+ `,
17
+ textareaContainer: css`
18
+ position: relative;
19
+ flex: 1;
20
+ `,
21
+ };
22
+ });
23
+
24
+ const InputArea = memo(() => {
25
+ const { t } = useTranslation('common');
26
+ const isChineseInput = useRef(false);
27
+
28
+ const { cx, styles } = useStyles();
29
+
30
+ const [loading, message, updateInputMessage] = useSessionStore((s) => [
31
+ !!s.chatLoadingId,
32
+ s.inputMessage,
33
+ s.updateInputMessage,
34
+ ]);
35
+
36
+ const handleSend = useSendMessage();
37
+
38
+ return (
39
+ <div className={cx(styles.textareaContainer)}>
40
+ <TextArea
41
+ className={styles.textarea}
42
+ onBlur={(e) => {
43
+ updateInputMessage(e.target.value);
44
+ }}
45
+ onChange={(e) => {
46
+ updateInputMessage(e.target.value);
47
+ }}
48
+ onCompositionEnd={() => {
49
+ isChineseInput.current = false;
50
+ }}
51
+ onCompositionStart={() => {
52
+ isChineseInput.current = true;
53
+ }}
54
+ onPressEnter={(e) => {
55
+ if (!loading && !e.shiftKey && !isChineseInput.current) {
56
+ e.preventDefault();
57
+ handleSend();
58
+ }
59
+ }}
60
+ placeholder={t('sendPlaceholder', { ns: 'chat' })}
61
+ resize={false}
62
+ type="pure"
63
+ value={message}
64
+ />
65
+ </div>
66
+ );
67
+ });
68
+
69
+ export default InputArea;
@@ -1,14 +1,34 @@
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 Footer from '@/app/chat/(desktop)/features/ChatInput/Footer';
7
+ import ActionBar from '@/app/chat/features/ChatInput/ActionBar';
4
8
  import { CHAT_TEXTAREA_HEIGHT, HEADER_HEIGHT } from '@/const/layoutTokens';
5
9
  import { useGlobalStore } from '@/store/global';
6
10
 
7
- import ChatInputContent from '../../../features/ChatInputContent';
8
- import Footer from './Footer';
11
+ import InputArea from './InputArea';
12
+
13
+ const useStyles = createStyles(({ css }) => {
14
+ return {
15
+ container: css`
16
+ position: relative;
17
+
18
+ display: flex;
19
+ flex-direction: column;
20
+ gap: 8px;
21
+
22
+ height: 100%;
23
+ padding: 12px 0 16px;
24
+ `,
25
+ };
26
+ });
9
27
 
10
28
  const ChatInputDesktopLayout = memo(() => {
29
+ const { styles } = useStyles();
11
30
  const [expand, setExpand] = useState<boolean>(false);
31
+
12
32
  const [inputHeight, updatePreference] = useGlobalStore((s) => [
13
33
  s.preference.inputHeight,
14
34
  s.updatePreference,
@@ -30,7 +50,20 @@ const ChatInputDesktopLayout = memo(() => {
30
50
  size={{ height: inputHeight, width: '100%' }}
31
51
  style={{ zIndex: 10 }}
32
52
  >
33
- <ChatInputContent expand={expand} footer={<Footer />} onExpandChange={setExpand} />
53
+ <section className={styles.container} style={{ minHeight: CHAT_TEXTAREA_HEIGHT }}>
54
+ <ActionBar
55
+ rightAreaEndRender={
56
+ <ActionIcon
57
+ icon={expand ? Minimize2 : Maximize2}
58
+ onClick={() => {
59
+ setExpand(!expand);
60
+ }}
61
+ />
62
+ }
63
+ />
64
+ <InputArea />
65
+ <Footer />
66
+ </section>
34
67
  </DraggablePanel>
35
68
  );
36
69
  });
@@ -0,0 +1,17 @@
1
+ import { useCallback } from 'react';
2
+
3
+ import { useSessionStore } from '@/store/session';
4
+
5
+ export const useSendMessage = () => {
6
+ const [sendMessage, updateInputMessage] = useSessionStore((s) => [
7
+ s.sendMessage,
8
+ s.updateInputMessage,
9
+ ]);
10
+
11
+ return useCallback(() => {
12
+ const store = useSessionStore.getState();
13
+ if (!!store.chatLoadingId) return;
14
+ sendMessage(store.inputMessage);
15
+ updateInputMessage('');
16
+ }, []);
17
+ };
@@ -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,39 @@
1
+ import { ActionIcon } from '@lobehub/ui';
2
+ import { Popconfirm } from 'antd';
3
+ import { Eraser } from 'lucide-react';
4
+ import { memo } 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 { useSessionStore } from '@/store/session';
11
+
12
+ const Clear = memo(() => {
13
+ const { t } = useTranslation('setting');
14
+ const [clearMessage] = useSessionStore((s) => [s.clearMessage, s.updateAgentConfig]);
15
+ const hotkeys = [PREFIX_KEY, CLEAN_MESSAGE_KEY].join('+');
16
+
17
+ useHotkeys(hotkeys, clearMessage, {
18
+ preventDefault: true,
19
+ });
20
+
21
+ return (
22
+ <Popconfirm
23
+ cancelText={t('cancel', { ns: 'common' })}
24
+ okButtonProps={{ danger: true }}
25
+ okText={t('ok', { ns: 'common' })}
26
+ onConfirm={() => clearMessage()}
27
+ placement={'topRight'}
28
+ title={t('confirmClearCurrentMessages', { ns: 'chat' })}
29
+ >
30
+ <ActionIcon
31
+ icon={Eraser}
32
+ placement={'bottom'}
33
+ title={(<HotKeys desc={t('clearCurrentMessages', { ns: 'chat' })} keys={hotkeys} />) as any}
34
+ />
35
+ </Popconfirm>
36
+ );
37
+ });
38
+
39
+ export default Clear;
@@ -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;
@@ -0,0 +1,42 @@
1
+ import { ActionIcon } from '@lobehub/ui';
2
+ import { Dropdown } from 'antd';
3
+ import { BrainCog } from 'lucide-react';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import { settingsSelectors, useGlobalStore } from '@/store/global';
8
+ import { useSessionStore } from '@/store/session';
9
+ import { agentSelectors } from '@/store/session/selectors';
10
+ import { LanguageModel } from '@/types/llm';
11
+
12
+ const ModelSwitch = memo(() => {
13
+ const { t } = useTranslation('setting');
14
+
15
+ const [model, updateAgentConfig] = useSessionStore((s) => {
16
+ const config = agentSelectors.currentAgentConfig(s);
17
+ return [config.model, s.updateAgentConfig];
18
+ });
19
+
20
+ const modelList = useGlobalStore(settingsSelectors.modelList);
21
+
22
+ return (
23
+ <Dropdown
24
+ menu={{
25
+ activeKey: model,
26
+ items: modelList.map((i) => ({ key: i, label: i })),
27
+ onClick: (e) => {
28
+ updateAgentConfig({ model: e.key as LanguageModel });
29
+ },
30
+ style: {
31
+ maxHeight: 400,
32
+ overflow: 'scroll',
33
+ },
34
+ }}
35
+ trigger={['click']}
36
+ >
37
+ <ActionIcon icon={BrainCog} placement={'bottom'} title={t('settingModel.model.title')} />
38
+ </Dropdown>
39
+ );
40
+ });
41
+
42
+ export default ModelSwitch;
@@ -0,0 +1,47 @@
1
+ import { ActionIcon, SliderWithInput } from '@lobehub/ui';
2
+ import { Popover } from 'antd';
3
+ import { Thermometer } from 'lucide-react';
4
+ import { memo } from 'react';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import { useSessionStore } from '@/store/session';
8
+ import { agentSelectors } from '@/store/session/selectors';
9
+
10
+ const Temperature = memo(() => {
11
+ const { t } = useTranslation('setting');
12
+
13
+ const [temperature, updateAgentConfig] = useSessionStore((s) => {
14
+ const config = agentSelectors.currentAgentConfig(s);
15
+ return [config.params.temperature, s.updateAgentConfig];
16
+ });
17
+
18
+ return (
19
+ <Popover
20
+ arrow={false}
21
+ content={
22
+ <SliderWithInput
23
+ controls={false}
24
+ max={1}
25
+ min={0}
26
+ onChange={(v) => {
27
+ updateAgentConfig({ params: { temperature: v } });
28
+ }}
29
+ size={'small'}
30
+ step={0.1}
31
+ style={{ width: 160 }}
32
+ value={temperature}
33
+ />
34
+ }
35
+ placement={'top'}
36
+ trigger={'click'}
37
+ >
38
+ <ActionIcon
39
+ icon={Thermometer}
40
+ placement={'bottom'}
41
+ title={t('settingModel.temperature.titleWithValue', { value: temperature })}
42
+ />
43
+ </Popover>
44
+ );
45
+ });
46
+
47
+ export default Temperature;
@@ -8,16 +8,16 @@ import { useSessionStore } from '@/store/session';
8
8
  import { agentSelectors, chatSelectors } from '@/store/session/selectors';
9
9
  import { LanguageModel } from '@/types/llm';
10
10
 
11
- const Token = memo<{ input: string }>(({ input }) => {
11
+ const Token = memo(() => {
12
12
  const { t } = useTranslation('chat');
13
13
 
14
- const inputTokenCount = useTokenCount(input);
15
-
16
- const [messageString, systemRole, model] = useSessionStore((s) => [
14
+ const [input, messageString, systemRole, model] = useSessionStore((s) => [
15
+ s.inputMessage,
17
16
  chatSelectors.chatsMessageString(s),
18
17
  agentSelectors.currentAgentSystemRole(s),
19
18
  agentSelectors.currentAgentModel(s) as LanguageModel,
20
19
  ]);
20
+ const inputTokenCount = useTokenCount(input);
21
21
 
22
22
  const systemRoleToken = useTokenCount(systemRole);
23
23
  const chatsToken = useTokenCount(messageString);
@@ -0,0 +1,21 @@
1
+ import dynamic from 'next/dynamic';
2
+ import { Suspense, memo } from 'react';
3
+
4
+ import { useSessionStore } from '@/store/session';
5
+ import { agentSelectors } from '@/store/session/selectors';
6
+
7
+ const LargeTokenContent = dynamic(() => import('./TokenTag'), { ssr: false });
8
+
9
+ const Token = memo(() => {
10
+ const [showTokenTag] = useSessionStore((s) => [agentSelectors.showTokenTag(s)]);
11
+
12
+ return (
13
+ showTokenTag && (
14
+ <Suspense>
15
+ <LargeTokenContent />
16
+ </Suspense>
17
+ )
18
+ );
19
+ });
20
+
21
+ export default Token;
@@ -0,0 +1,19 @@
1
+ import { FC } from 'react';
2
+
3
+ import Clear from './Clear';
4
+ import History from './History';
5
+ import ModelSwitch from './ModelSwitch';
6
+ import Temperature from './Temperature';
7
+ import Token from './Token';
8
+
9
+ export const actionMap: Record<string, FC> = {
10
+ clear: Clear,
11
+ history: History,
12
+ model: ModelSwitch,
13
+ temperature: Temperature,
14
+ token: Token,
15
+ };
16
+
17
+ // we can make these action lists configurable in the future
18
+ export const leftActionList = ['model', 'temperature', 'history', 'token'];
19
+ export const rightActionList = ['clear'];
@@ -0,0 +1,35 @@
1
+ import { ReactNode, memo } from 'react';
2
+ import { Flexbox } from 'react-layout-kit';
3
+
4
+ import { actionMap, leftActionList, rightActionList } from './config';
5
+
6
+ const RenderActionList = ({ dataSource }: { dataSource: string[] }) => (
7
+ <>
8
+ {dataSource.map((key) => {
9
+ const Render = actionMap[key];
10
+ return <Render key={key} />;
11
+ })}
12
+ </>
13
+ );
14
+
15
+ export interface ActionBarProps {
16
+ rightAreaEndRender?: ReactNode;
17
+ rightAreaStartRender?: ReactNode;
18
+ }
19
+
20
+ const ActionBar = memo<ActionBarProps>(({ rightAreaStartRender, rightAreaEndRender }) => {
21
+ return (
22
+ <Flexbox align={'center'} flex={'none'} horizontal justify={'space-between'} padding={'0 16px'}>
23
+ <Flexbox align={'center'} flex={1} gap={4} horizontal>
24
+ <RenderActionList dataSource={leftActionList} />
25
+ </Flexbox>
26
+ <Flexbox align={'center'} flex={0} gap={4} horizontal justify={'flex-end'}>
27
+ {rightAreaStartRender}
28
+ <RenderActionList dataSource={rightActionList} />
29
+ {rightAreaEndRender}
30
+ </Flexbox>
31
+ </Flexbox>
32
+ );
33
+ });
34
+
35
+ export default ActionBar;
@@ -25,6 +25,7 @@ const SaveTopic = memo(() => {
25
25
 
26
26
  const hotkeys = [PREFIX_KEY, SAVE_TOPIC_KEY].join('+');
27
27
  useHotkeys(hotkeys, openNewTopicOrSaveTopic, {
28
+ enableOnFormTags: true,
28
29
  preventDefault: true,
29
30
  });
30
31
 
@@ -68,12 +68,13 @@ export interface ChatMessageAction {
68
68
  */
69
69
  sendMessage: (text: string) => Promise<void>;
70
70
  stopGenerateMessage: () => void;
71
-
72
71
  toggleChatLoading: (
73
72
  loading: boolean,
74
73
  id?: string,
75
74
  action?: string,
76
75
  ) => AbortController | undefined;
76
+
77
+ updateInputMessage: (message: string) => void;
77
78
  }
78
79
 
79
80
  export const chatMessage: StateCreator<
@@ -94,7 +95,6 @@ export const chatMessage: StateCreator<
94
95
  // after remove topic , go back to default topic
95
96
  toggleTopic();
96
97
  },
97
-
98
98
  coreProcessMessage: async (messages, userMessageId) => {
99
99
  const { dispatchMessage, fetchAIChatMessage, triggerFunctionCall, activeTopicId } = get();
100
100
 
@@ -163,6 +163,7 @@ export const chatMessage: StateCreator<
163
163
  deleteMessage: (id) => {
164
164
  get().dispatchMessage({ id, type: 'deleteMessage' });
165
165
  },
166
+
166
167
  dispatchMessage: (payload) => {
167
168
  const { activeId } = get();
168
169
  const session = sessionSelectors.currentSession(get());
@@ -172,7 +173,6 @@ export const chatMessage: StateCreator<
172
173
 
173
174
  get().dispatchSession({ chats, id: activeId, type: 'updateSessionChat' });
174
175
  },
175
-
176
176
  fetchAIChatMessage: async (messages, assistantId) => {
177
177
  const { dispatchMessage, toggleChatLoading } = get();
178
178
 
@@ -341,6 +341,7 @@ export const chatMessage: StateCreator<
341
341
 
342
342
  toggleChatLoading(false);
343
343
  },
344
+
344
345
  toggleChatLoading: (loading, id, action) => {
345
346
  if (loading) {
346
347
  const abortController = new AbortController();
@@ -350,4 +351,7 @@ export const chatMessage: StateCreator<
350
351
  set({ abortController: undefined, chatLoadingId: undefined }, false, action);
351
352
  }
352
353
  },
354
+ updateInputMessage: (message) => {
355
+ set({ inputMessage: message }, false, t('updateInputMessage'));
356
+ },
353
357
  });
@@ -2,9 +2,12 @@ export interface ChatState {
2
2
  abortController?: AbortController;
3
3
  activeTopicId?: string;
4
4
  chatLoadingId?: string;
5
+ inputMessage: string;
5
6
  renameTopicId?: string;
6
7
  shareLoading?: boolean;
7
8
  topicLoadingId?: string;
8
9
  }
9
10
 
10
- export const initialChatState: ChatState = {};
11
+ export const initialChatState: ChatState = {
12
+ inputMessage: '',
13
+ };
@@ -41,6 +41,7 @@ export const currentChats = (s: SessionStore): ChatMessage[] => {
41
41
  return getChatsById(s.activeId)(s);
42
42
  };
43
43
 
44
+ const initTime = Date.now();
44
45
  // 针对新助手添加初始化时的自定义消息
45
46
  export const currentChatsWithGuideMessage = (s: SessionStore): ChatMessage[] => {
46
47
  const data = currentChats(s);
@@ -67,14 +68,14 @@ export const currentChatsWithGuideMessage = (s: SessionStore): ChatMessage[] =>
67
68
 
68
69
  const emptyInboxGuideMessage = {
69
70
  content: isInbox ? inboxMsg : !!meta.description ? agentSystemRoleMsg : agentMsg,
70
- createAt: Date.now(),
71
+ createAt: initTime,
71
72
  extra: {},
72
73
  id: 'default',
73
74
  meta: meta || {
74
75
  avatar: DEFAULT_INBOX_AVATAR,
75
76
  },
76
77
  role: 'assistant',
77
- updateAt: Date.now(),
78
+ updateAt: initTime,
78
79
  } as ChatMessage;
79
80
 
80
81
  return [emptyInboxGuideMessage];
@@ -1,117 +0,0 @@
1
- import { ActionIcon, SliderWithInput } from '@lobehub/ui';
2
- import { Dropdown, Popover, Switch } from 'antd';
3
- import { BrainCog, Thermometer, 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 { settingsSelectors, useGlobalStore } from '@/store/global';
9
- import { useSessionStore } from '@/store/session';
10
- import { agentSelectors } from '@/store/session/selectors';
11
- import { LanguageModel } from '@/types/llm';
12
-
13
- const ActionLeft = memo(() => {
14
- const { t } = useTranslation('setting');
15
-
16
- const [model, temperature, historyCount, unlimited, updateAgentConfig] = useSessionStore((s) => {
17
- const config = agentSelectors.currentAgentConfig(s);
18
- return [
19
- config.model,
20
- config.params.temperature,
21
- config.historyCount,
22
- !config.enableHistoryCount,
23
- s.updateAgentConfig,
24
- ];
25
- });
26
-
27
- const modelList = useGlobalStore(settingsSelectors.modelList);
28
-
29
- return (
30
- <>
31
- <Dropdown
32
- menu={{
33
- activeKey: model,
34
- items: modelList.map((i) => ({ key: i, label: i })),
35
- onClick: (e) => {
36
- updateAgentConfig({ model: e.key as LanguageModel });
37
- },
38
- style: {
39
- maxHeight: 400,
40
- overflow: 'scroll',
41
- },
42
- }}
43
- trigger={['click']}
44
- >
45
- <ActionIcon icon={BrainCog} placement={'bottom'} title={t('settingModel.model.title')} />
46
- </Dropdown>
47
- <Popover
48
- arrow={false}
49
- content={
50
- <SliderWithInput
51
- controls={false}
52
- max={1}
53
- min={0}
54
- onChange={(v) => {
55
- updateAgentConfig({ params: { temperature: v } });
56
- }}
57
- size={'small'}
58
- step={0.1}
59
- style={{ width: 160 }}
60
- value={temperature}
61
- />
62
- }
63
- placement={'top'}
64
- trigger={'click'}
65
- >
66
- <ActionIcon
67
- icon={Thermometer}
68
- placement={'bottom'}
69
- title={t('settingModel.temperature.titleWithValue', { value: temperature })}
70
- />
71
- </Popover>
72
- <Popover
73
- arrow={false}
74
- content={
75
- <Flexbox align={'center'} gap={16} horizontal>
76
- <SliderWithInput
77
- disabled={unlimited}
78
- max={30}
79
- min={1}
80
- onChange={(v) => {
81
- updateAgentConfig({ historyCount: v });
82
- }}
83
- step={1}
84
- style={{ width: 160 }}
85
- value={historyCount}
86
- />
87
- <Flexbox align={'center'} gap={4} horizontal>
88
- <Switch
89
- checked={unlimited}
90
- onChange={(checked) => {
91
- updateAgentConfig({ enableHistoryCount: !checked });
92
- }}
93
- size={'small'}
94
- />
95
- {t('settingChat.enableHistoryCount.alias')}
96
- </Flexbox>
97
- </Flexbox>
98
- }
99
- placement={'top'}
100
- trigger={'click'}
101
- >
102
- <ActionIcon
103
- icon={unlimited ? TimerOff : Timer}
104
- placement={'bottom'}
105
- title={t(
106
- unlimited
107
- ? 'settingChat.enableHistoryCount.unlimited'
108
- : 'settingChat.enableHistoryCount.limited',
109
- { number: historyCount || 0 },
110
- )}
111
- />
112
- </Popover>
113
- </>
114
- );
115
- });
116
-
117
- export default ActionLeft;
@@ -1,48 +0,0 @@
1
- import { ActionIcon } from '@lobehub/ui';
2
- import { Popconfirm } from 'antd';
3
- import { useResponsive } from 'antd-style';
4
- import { Eraser } from 'lucide-react';
5
- import { memo } from 'react';
6
- import { useHotkeys } from 'react-hotkeys-hook';
7
- import { useTranslation } from 'react-i18next';
8
-
9
- import HotKeys from '@/components/HotKeys';
10
- import { CLEAN_MESSAGE_KEY, PREFIX_KEY } from '@/const/hotkeys';
11
- import { useSessionStore } from '@/store/session';
12
-
13
- import SaveTopic from '../Topic';
14
-
15
- const ActionsRight = memo(() => {
16
- const { t } = useTranslation('setting');
17
- const [clearMessage] = useSessionStore((s) => [s.clearMessage, s.updateAgentConfig]);
18
- const { mobile } = useResponsive();
19
- const hotkeys = [PREFIX_KEY, CLEAN_MESSAGE_KEY].join('+');
20
-
21
- useHotkeys(hotkeys, clearMessage, {
22
- preventDefault: true,
23
- });
24
-
25
- return (
26
- <>
27
- {mobile && <SaveTopic />}
28
- <Popconfirm
29
- cancelText={t('cancel', { ns: 'common' })}
30
- okButtonProps={{ danger: true }}
31
- okText={t('ok', { ns: 'common' })}
32
- onConfirm={() => clearMessage()}
33
- placement={'topRight'}
34
- title={t('confirmClearCurrentMessages', { ns: 'chat' })}
35
- >
36
- <ActionIcon
37
- icon={Eraser}
38
- placement={'bottom'}
39
- title={
40
- (<HotKeys desc={t('clearCurrentMessages', { ns: 'chat' })} keys={hotkeys} />) as any
41
- }
42
- />
43
- </Popconfirm>
44
- </>
45
- );
46
- });
47
-
48
- export default ActionsRight;
@@ -1,71 +0,0 @@
1
- import { ChatInputArea } from '@lobehub/ui';
2
- import { useResponsive } from 'antd-style';
3
- import dynamic from 'next/dynamic';
4
- import { ReactNode, Suspense, memo, useState } from 'react';
5
- import { useTranslation } from 'react-i18next';
6
-
7
- import { CHAT_TEXTAREA_HEIGHT } from '@/const/layoutTokens';
8
- import { useSessionStore } from '@/store/session';
9
- import { agentSelectors } from '@/store/session/selectors';
10
-
11
- import ActionLeft from './ActionBar/ActionLeft';
12
- import ActionsRight from './ActionBar/ActionRight';
13
-
14
- const Token = dynamic(() => import('./ActionBar/Token'), { ssr: false });
15
-
16
- interface ChatContentProps {
17
- expand?: boolean;
18
- footer?: ReactNode;
19
- mobile?: boolean;
20
- onExpandChange?: (expand: boolean) => void;
21
- }
22
-
23
- const ChatInputContent = memo<ChatContentProps>(
24
- ({ expand, onExpandChange, mobile: defaultMobile, footer }) => {
25
- const { t } = useTranslation('common');
26
-
27
- const [message, setMessage] = useState('');
28
- const { mobile: runtimeMobile } = useResponsive();
29
-
30
- const mobile = runtimeMobile || defaultMobile;
31
-
32
- const [isLoading, sendMessage, stopGenerateMessage, showTokenTag] = useSessionStore((s) => [
33
- !!s.chatLoadingId,
34
- s.sendMessage,
35
- s.stopGenerateMessage,
36
- agentSelectors.showTokenTag(s),
37
- ]);
38
-
39
- return (
40
- <ChatInputArea
41
- actions={
42
- <>
43
- <ActionLeft />
44
- {showTokenTag && (
45
- <Suspense>
46
- <Token input={message} />
47
- </Suspense>
48
- )}
49
- </>
50
- }
51
- actionsRight={<ActionsRight />}
52
- expand={expand}
53
- footer={footer}
54
- loading={isLoading}
55
- minHeight={mobile ? 0 : CHAT_TEXTAREA_HEIGHT}
56
- onExpandChange={onExpandChange}
57
- onInputChange={setMessage}
58
- onSend={sendMessage}
59
- onStop={stopGenerateMessage}
60
- placeholder={t('sendPlaceholder', { ns: 'chat' })}
61
- text={{
62
- send: t('send'),
63
- stop: t('stop'),
64
- }}
65
- value={message}
66
- />
67
- );
68
- },
69
- );
70
-
71
- export default ChatInputContent;