mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
♻️ refactor: refactor the input area to suit the files upload feature (#442)
* ♻️ refactor: refactor the input area from ui * ♻️ refactor: refactor ActionBar to a configurable stage
This commit is contained in:
@@ -1,18 +1,37 @@
|
||||
import { Icon } from '@lobehub/ui';
|
||||
import { useTheme } from 'antd-style';
|
||||
import { ArrowBigUp, CornerDownLeft } from 'lucide-react';
|
||||
import { Button } from 'antd';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { ArrowBigUp, CornerDownLeft, Loader2 } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import SaveTopic from '../../../features/ChatInputContent/Topic';
|
||||
import SaveTopic from '@/app/chat/features/ChatInput/Topic';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
|
||||
import { useSendMessage } from './useSend';
|
||||
|
||||
const useStyles = createStyles(({ css }) => ({
|
||||
footerBar: css`
|
||||
display: flex;
|
||||
flex: none;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
padding: 0 24px;
|
||||
`,
|
||||
}));
|
||||
|
||||
const Footer = memo(() => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation('chat');
|
||||
const { styles, theme } = useStyles();
|
||||
const [loading, onStop] = useSessionStore((s) => [!!s.chatLoadingId, s.stopGenerateMessage]);
|
||||
|
||||
const onSend = useSendMessage();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.footerBar}>
|
||||
<Flexbox
|
||||
gap={4}
|
||||
horizontal
|
||||
@@ -28,7 +47,16 @@ const Footer = memo(() => {
|
||||
<span>{t('warp')}</span>
|
||||
</Flexbox>
|
||||
<SaveTopic />
|
||||
</>
|
||||
{loading ? (
|
||||
<Button icon={loading && <Icon icon={Loader2} spin />} onClick={onStop}>
|
||||
{t('stop')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={() => onSend()} type={'primary'}>
|
||||
{t('send')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { TextArea } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useSessionStore } from '@/store/session';
|
||||
|
||||
import { useSendMessage } from './useSend';
|
||||
|
||||
const useStyles = createStyles(({ css }) => {
|
||||
return {
|
||||
textarea: css`
|
||||
height: 100% !important;
|
||||
padding: 0 24px;
|
||||
line-height: 1.5;
|
||||
`,
|
||||
textareaContainer: css`
|
||||
position: relative;
|
||||
flex: 1;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const InputArea = memo(() => {
|
||||
const { t } = useTranslation('common');
|
||||
const isChineseInput = useRef(false);
|
||||
|
||||
const { cx, styles } = useStyles();
|
||||
|
||||
const [loading, message, updateInputMessage] = useSessionStore((s) => [
|
||||
!!s.chatLoadingId,
|
||||
s.inputMessage,
|
||||
s.updateInputMessage,
|
||||
]);
|
||||
|
||||
const handleSend = useSendMessage();
|
||||
|
||||
return (
|
||||
<div className={cx(styles.textareaContainer)}>
|
||||
<TextArea
|
||||
className={styles.textarea}
|
||||
onBlur={(e) => {
|
||||
updateInputMessage(e.target.value);
|
||||
}}
|
||||
onChange={(e) => {
|
||||
updateInputMessage(e.target.value);
|
||||
}}
|
||||
onCompositionEnd={() => {
|
||||
isChineseInput.current = false;
|
||||
}}
|
||||
onCompositionStart={() => {
|
||||
isChineseInput.current = true;
|
||||
}}
|
||||
onPressEnter={(e) => {
|
||||
if (!loading && !e.shiftKey && !isChineseInput.current) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}}
|
||||
placeholder={t('sendPlaceholder', { ns: 'chat' })}
|
||||
resize={false}
|
||||
type="pure"
|
||||
value={message}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default InputArea;
|
||||
@@ -1,14 +1,34 @@
|
||||
import { DraggablePanel } from '@lobehub/ui';
|
||||
import { ActionIcon, DraggablePanel } from '@lobehub/ui';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { Maximize2, Minimize2 } from 'lucide-react';
|
||||
import { memo, useState } from 'react';
|
||||
|
||||
import Footer from '@/app/chat/(desktop)/features/ChatInput/Footer';
|
||||
import ActionBar from '@/app/chat/features/ChatInput/ActionBar';
|
||||
import { CHAT_TEXTAREA_HEIGHT, HEADER_HEIGHT } from '@/const/layoutTokens';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
|
||||
import ChatInputContent from '../../../features/ChatInputContent';
|
||||
import Footer from './Footer';
|
||||
import InputArea from './InputArea';
|
||||
|
||||
const useStyles = createStyles(({ css }) => {
|
||||
return {
|
||||
container: css`
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
height: 100%;
|
||||
padding: 12px 0 16px;
|
||||
`,
|
||||
};
|
||||
});
|
||||
|
||||
const ChatInputDesktopLayout = memo(() => {
|
||||
const { styles } = useStyles();
|
||||
const [expand, setExpand] = useState<boolean>(false);
|
||||
|
||||
const [inputHeight, updatePreference] = useGlobalStore((s) => [
|
||||
s.preference.inputHeight,
|
||||
s.updatePreference,
|
||||
@@ -30,7 +50,20 @@ const ChatInputDesktopLayout = memo(() => {
|
||||
size={{ height: inputHeight, width: '100%' }}
|
||||
style={{ zIndex: 10 }}
|
||||
>
|
||||
<ChatInputContent expand={expand} footer={<Footer />} onExpandChange={setExpand} />
|
||||
<section className={styles.container} style={{ minHeight: CHAT_TEXTAREA_HEIGHT }}>
|
||||
<ActionBar
|
||||
rightAreaEndRender={
|
||||
<ActionIcon
|
||||
icon={expand ? Minimize2 : Maximize2}
|
||||
onClick={() => {
|
||||
setExpand(!expand);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<InputArea />
|
||||
<Footer />
|
||||
</section>
|
||||
</DraggablePanel>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useSessionStore } from '@/store/session';
|
||||
|
||||
export const useSendMessage = () => {
|
||||
const [sendMessage, updateInputMessage] = useSessionStore((s) => [
|
||||
s.sendMessage,
|
||||
s.updateInputMessage,
|
||||
]);
|
||||
|
||||
return useCallback(() => {
|
||||
const store = useSessionStore.getState();
|
||||
if (!!store.chatLoadingId) return;
|
||||
sendMessage(store.inputMessage);
|
||||
updateInputMessage('');
|
||||
}, []);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Icon, Input } from '@lobehub/ui';
|
||||
import { Button, type InputRef } from 'antd';
|
||||
import { Loader2, SendHorizonal } from 'lucide-react';
|
||||
import { forwardRef, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import useControlledState from 'use-merge-value';
|
||||
|
||||
import ActionBar from '@/app/chat/features/ChatInput/ActionBar';
|
||||
import SaveTopic from '@/app/chat/features/ChatInput/Topic';
|
||||
|
||||
import { useStyles } from './style.mobile';
|
||||
|
||||
export type ChatInputAreaMobile = {
|
||||
loading?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
onSend?: (value: string) => void;
|
||||
onStop?: () => void;
|
||||
value?: string;
|
||||
};
|
||||
|
||||
const ChatInputArea = forwardRef<InputRef, ChatInputAreaMobile>(
|
||||
({ onSend, loading, onChange, onStop, value }) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const [currentValue, setCurrentValue] = useControlledState<string>('', {
|
||||
onChange: onChange,
|
||||
value,
|
||||
});
|
||||
const { cx, styles } = useStyles();
|
||||
const isChineseInput = useRef(false);
|
||||
|
||||
const handleSend = useCallback(() => {
|
||||
if (loading) return;
|
||||
if (onSend) onSend(currentValue);
|
||||
setCurrentValue('');
|
||||
}, [currentValue]);
|
||||
|
||||
return (
|
||||
<Flexbox className={cx(styles.container)} gap={12}>
|
||||
<ActionBar rightAreaStartRender={<SaveTopic />} />
|
||||
<Flexbox className={styles.inner} gap={8} horizontal>
|
||||
<Input
|
||||
className={cx(styles.input)}
|
||||
onBlur={(e) => {
|
||||
setCurrentValue(e.target.value);
|
||||
}}
|
||||
onChange={(e) => {
|
||||
setCurrentValue(e.target.value);
|
||||
}}
|
||||
onCompositionEnd={() => {
|
||||
isChineseInput.current = false;
|
||||
}}
|
||||
onCompositionStart={() => {
|
||||
isChineseInput.current = true;
|
||||
}}
|
||||
onPressEnter={(e) => {
|
||||
if (!loading && !e.shiftKey && !isChineseInput.current) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
}
|
||||
}}
|
||||
placeholder={t('sendPlaceholder')}
|
||||
type={'block'}
|
||||
value={currentValue}
|
||||
/>
|
||||
<div>
|
||||
{loading ? (
|
||||
<Button icon={loading && <Icon icon={Loader2} spin />} onClick={onStop} />
|
||||
) : (
|
||||
<Button icon={<Icon icon={SendHorizonal} />} onClick={handleSend} type={'primary'} />
|
||||
)}
|
||||
</div>
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default ChatInputArea;
|
||||
+19
-3
@@ -1,10 +1,11 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
import { memo, useState } from 'react';
|
||||
|
||||
import SafeSpacing from '@/components/SafeSpacing';
|
||||
import { CHAT_TEXTAREA_HEIGHT_MOBILE } from '@/const/layoutTokens';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
|
||||
import ChatInputContent from '../../features/ChatInputContent';
|
||||
import ChatInputArea from './Mobile';
|
||||
|
||||
const useStyles = createStyles(
|
||||
({ css, token }) => css`
|
||||
@@ -21,11 +22,26 @@ const useStyles = createStyles(
|
||||
|
||||
const ChatInputMobileLayout = memo(() => {
|
||||
const { styles } = useStyles();
|
||||
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
const [isLoading, sendMessage, stopGenerateMessage] = useSessionStore((s) => [
|
||||
!!s.chatLoadingId,
|
||||
s.sendMessage,
|
||||
s.stopGenerateMessage,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SafeSpacing height={CHAT_TEXTAREA_HEIGHT_MOBILE} mobile position={'bottom'} />
|
||||
<div className={styles}>
|
||||
<ChatInputContent mobile />
|
||||
<ChatInputArea
|
||||
loading={isLoading}
|
||||
onChange={setMessage}
|
||||
onSend={sendMessage}
|
||||
onStop={stopGenerateMessage}
|
||||
value={message}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
import { rgba } from 'polished';
|
||||
|
||||
export const useStyles = createStyles(({ css, token }) => {
|
||||
return {
|
||||
container: css`
|
||||
padding: 12px 0;
|
||||
background: ${token.colorBgLayout};
|
||||
border-top: 1px solid ${rgba(token.colorBorder, 0.25)};
|
||||
`,
|
||||
inner: css`
|
||||
padding: 0 16px;
|
||||
`,
|
||||
input: css`
|
||||
background: ${token.colorFillSecondary} !important;
|
||||
border: none !important;
|
||||
`,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ActionIcon } from '@lobehub/ui';
|
||||
import { Popconfirm } from 'antd';
|
||||
import { Eraser } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import HotKeys from '@/components/HotKeys';
|
||||
import { CLEAN_MESSAGE_KEY, PREFIX_KEY } from '@/const/hotkeys';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
|
||||
const Clear = memo(() => {
|
||||
const { t } = useTranslation('setting');
|
||||
const [clearMessage] = useSessionStore((s) => [s.clearMessage, s.updateAgentConfig]);
|
||||
const hotkeys = [PREFIX_KEY, CLEAN_MESSAGE_KEY].join('+');
|
||||
|
||||
useHotkeys(hotkeys, clearMessage, {
|
||||
preventDefault: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<Popconfirm
|
||||
cancelText={t('cancel', { ns: 'common' })}
|
||||
okButtonProps={{ danger: true }}
|
||||
okText={t('ok', { ns: 'common' })}
|
||||
onConfirm={() => clearMessage()}
|
||||
placement={'topRight'}
|
||||
title={t('confirmClearCurrentMessages', { ns: 'chat' })}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={Eraser}
|
||||
placement={'bottom'}
|
||||
title={(<HotKeys desc={t('clearCurrentMessages', { ns: 'chat' })} keys={hotkeys} />) as any}
|
||||
/>
|
||||
</Popconfirm>
|
||||
);
|
||||
});
|
||||
|
||||
export default Clear;
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ActionIcon, SliderWithInput } from '@lobehub/ui';
|
||||
import { Popover, Switch } from 'antd';
|
||||
import { Timer, TimerOff } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { agentSelectors } from '@/store/session/selectors';
|
||||
|
||||
const History = memo(() => {
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
const [historyCount, unlimited, updateAgentConfig] = useSessionStore((s) => {
|
||||
const config = agentSelectors.currentAgentConfig(s);
|
||||
return [config.historyCount, !config.enableHistoryCount, s.updateAgentConfig];
|
||||
});
|
||||
|
||||
return (
|
||||
<Popover
|
||||
arrow={false}
|
||||
content={
|
||||
<Flexbox align={'center'} gap={16} horizontal>
|
||||
<SliderWithInput
|
||||
disabled={unlimited}
|
||||
max={30}
|
||||
min={1}
|
||||
onChange={(v) => {
|
||||
updateAgentConfig({ historyCount: v });
|
||||
}}
|
||||
step={1}
|
||||
style={{ width: 160 }}
|
||||
value={historyCount}
|
||||
/>
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
<Switch
|
||||
checked={unlimited}
|
||||
onChange={(checked) => {
|
||||
updateAgentConfig({ enableHistoryCount: !checked });
|
||||
}}
|
||||
size={'small'}
|
||||
/>
|
||||
{t('settingChat.enableHistoryCount.alias')}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
}
|
||||
placement={'top'}
|
||||
trigger={'click'}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={unlimited ? TimerOff : Timer}
|
||||
placement={'bottom'}
|
||||
title={t(
|
||||
unlimited
|
||||
? 'settingChat.enableHistoryCount.unlimited'
|
||||
: 'settingChat.enableHistoryCount.limited',
|
||||
{ number: historyCount || 0 },
|
||||
)}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
export default History;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ActionIcon } from '@lobehub/ui';
|
||||
import { Dropdown } from 'antd';
|
||||
import { BrainCog } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { settingsSelectors, useGlobalStore } from '@/store/global';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { agentSelectors } from '@/store/session/selectors';
|
||||
import { LanguageModel } from '@/types/llm';
|
||||
|
||||
const ModelSwitch = memo(() => {
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
const [model, updateAgentConfig] = useSessionStore((s) => {
|
||||
const config = agentSelectors.currentAgentConfig(s);
|
||||
return [config.model, s.updateAgentConfig];
|
||||
});
|
||||
|
||||
const modelList = useGlobalStore(settingsSelectors.modelList);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{
|
||||
activeKey: model,
|
||||
items: modelList.map((i) => ({ key: i, label: i })),
|
||||
onClick: (e) => {
|
||||
updateAgentConfig({ model: e.key as LanguageModel });
|
||||
},
|
||||
style: {
|
||||
maxHeight: 400,
|
||||
overflow: 'scroll',
|
||||
},
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<ActionIcon icon={BrainCog} placement={'bottom'} title={t('settingModel.model.title')} />
|
||||
</Dropdown>
|
||||
);
|
||||
});
|
||||
|
||||
export default ModelSwitch;
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ActionIcon, SliderWithInput } from '@lobehub/ui';
|
||||
import { Popover } from 'antd';
|
||||
import { Thermometer } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { agentSelectors } from '@/store/session/selectors';
|
||||
|
||||
const Temperature = memo(() => {
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
const [temperature, updateAgentConfig] = useSessionStore((s) => {
|
||||
const config = agentSelectors.currentAgentConfig(s);
|
||||
return [config.params.temperature, s.updateAgentConfig];
|
||||
});
|
||||
|
||||
return (
|
||||
<Popover
|
||||
arrow={false}
|
||||
content={
|
||||
<SliderWithInput
|
||||
controls={false}
|
||||
max={1}
|
||||
min={0}
|
||||
onChange={(v) => {
|
||||
updateAgentConfig({ params: { temperature: v } });
|
||||
}}
|
||||
size={'small'}
|
||||
step={0.1}
|
||||
style={{ width: 160 }}
|
||||
value={temperature}
|
||||
/>
|
||||
}
|
||||
placement={'top'}
|
||||
trigger={'click'}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={Thermometer}
|
||||
placement={'bottom'}
|
||||
title={t('settingModel.temperature.titleWithValue', { value: temperature })}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
|
||||
export default Temperature;
|
||||
+4
-4
@@ -8,16 +8,16 @@ import { useSessionStore } from '@/store/session';
|
||||
import { agentSelectors, chatSelectors } from '@/store/session/selectors';
|
||||
import { LanguageModel } from '@/types/llm';
|
||||
|
||||
const Token = memo<{ input: string }>(({ input }) => {
|
||||
const Token = memo(() => {
|
||||
const { t } = useTranslation('chat');
|
||||
|
||||
const inputTokenCount = useTokenCount(input);
|
||||
|
||||
const [messageString, systemRole, model] = useSessionStore((s) => [
|
||||
const [input, messageString, systemRole, model] = useSessionStore((s) => [
|
||||
s.inputMessage,
|
||||
chatSelectors.chatsMessageString(s),
|
||||
agentSelectors.currentAgentSystemRole(s),
|
||||
agentSelectors.currentAgentModel(s) as LanguageModel,
|
||||
]);
|
||||
const inputTokenCount = useTokenCount(input);
|
||||
|
||||
const systemRoleToken = useTokenCount(systemRole);
|
||||
const chatsToken = useTokenCount(messageString);
|
||||
@@ -0,0 +1,21 @@
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Suspense, memo } from 'react';
|
||||
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { agentSelectors } from '@/store/session/selectors';
|
||||
|
||||
const LargeTokenContent = dynamic(() => import('./TokenTag'), { ssr: false });
|
||||
|
||||
const Token = memo(() => {
|
||||
const [showTokenTag] = useSessionStore((s) => [agentSelectors.showTokenTag(s)]);
|
||||
|
||||
return (
|
||||
showTokenTag && (
|
||||
<Suspense>
|
||||
<LargeTokenContent />
|
||||
</Suspense>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
export default Token;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
import Clear from './Clear';
|
||||
import History from './History';
|
||||
import ModelSwitch from './ModelSwitch';
|
||||
import Temperature from './Temperature';
|
||||
import Token from './Token';
|
||||
|
||||
export const actionMap: Record<string, FC> = {
|
||||
clear: Clear,
|
||||
history: History,
|
||||
model: ModelSwitch,
|
||||
temperature: Temperature,
|
||||
token: Token,
|
||||
};
|
||||
|
||||
// we can make these action lists configurable in the future
|
||||
export const leftActionList = ['model', 'temperature', 'history', 'token'];
|
||||
export const rightActionList = ['clear'];
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ReactNode, memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { actionMap, leftActionList, rightActionList } from './config';
|
||||
|
||||
const RenderActionList = ({ dataSource }: { dataSource: string[] }) => (
|
||||
<>
|
||||
{dataSource.map((key) => {
|
||||
const Render = actionMap[key];
|
||||
return <Render key={key} />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
export interface ActionBarProps {
|
||||
rightAreaEndRender?: ReactNode;
|
||||
rightAreaStartRender?: ReactNode;
|
||||
}
|
||||
|
||||
const ActionBar = memo<ActionBarProps>(({ rightAreaStartRender, rightAreaEndRender }) => {
|
||||
return (
|
||||
<Flexbox align={'center'} flex={'none'} horizontal justify={'space-between'} padding={'0 16px'}>
|
||||
<Flexbox align={'center'} flex={1} gap={4} horizontal>
|
||||
<RenderActionList dataSource={leftActionList} />
|
||||
</Flexbox>
|
||||
<Flexbox align={'center'} flex={0} gap={4} horizontal justify={'flex-end'}>
|
||||
{rightAreaStartRender}
|
||||
<RenderActionList dataSource={rightActionList} />
|
||||
{rightAreaEndRender}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
|
||||
export default ActionBar;
|
||||
@@ -1,117 +0,0 @@
|
||||
import { ActionIcon, SliderWithInput } from '@lobehub/ui';
|
||||
import { Dropdown, Popover, Switch } from 'antd';
|
||||
import { BrainCog, Thermometer, Timer, TimerOff } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { settingsSelectors, useGlobalStore } from '@/store/global';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { agentSelectors } from '@/store/session/selectors';
|
||||
import { LanguageModel } from '@/types/llm';
|
||||
|
||||
const ActionLeft = memo(() => {
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
const [model, temperature, historyCount, unlimited, updateAgentConfig] = useSessionStore((s) => {
|
||||
const config = agentSelectors.currentAgentConfig(s);
|
||||
return [
|
||||
config.model,
|
||||
config.params.temperature,
|
||||
config.historyCount,
|
||||
!config.enableHistoryCount,
|
||||
s.updateAgentConfig,
|
||||
];
|
||||
});
|
||||
|
||||
const modelList = useGlobalStore(settingsSelectors.modelList);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown
|
||||
menu={{
|
||||
activeKey: model,
|
||||
items: modelList.map((i) => ({ key: i, label: i })),
|
||||
onClick: (e) => {
|
||||
updateAgentConfig({ model: e.key as LanguageModel });
|
||||
},
|
||||
style: {
|
||||
maxHeight: 400,
|
||||
overflow: 'scroll',
|
||||
},
|
||||
}}
|
||||
trigger={['click']}
|
||||
>
|
||||
<ActionIcon icon={BrainCog} placement={'bottom'} title={t('settingModel.model.title')} />
|
||||
</Dropdown>
|
||||
<Popover
|
||||
arrow={false}
|
||||
content={
|
||||
<SliderWithInput
|
||||
controls={false}
|
||||
max={1}
|
||||
min={0}
|
||||
onChange={(v) => {
|
||||
updateAgentConfig({ params: { temperature: v } });
|
||||
}}
|
||||
size={'small'}
|
||||
step={0.1}
|
||||
style={{ width: 160 }}
|
||||
value={temperature}
|
||||
/>
|
||||
}
|
||||
placement={'top'}
|
||||
trigger={'click'}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={Thermometer}
|
||||
placement={'bottom'}
|
||||
title={t('settingModel.temperature.titleWithValue', { value: temperature })}
|
||||
/>
|
||||
</Popover>
|
||||
<Popover
|
||||
arrow={false}
|
||||
content={
|
||||
<Flexbox align={'center'} gap={16} horizontal>
|
||||
<SliderWithInput
|
||||
disabled={unlimited}
|
||||
max={30}
|
||||
min={1}
|
||||
onChange={(v) => {
|
||||
updateAgentConfig({ historyCount: v });
|
||||
}}
|
||||
step={1}
|
||||
style={{ width: 160 }}
|
||||
value={historyCount}
|
||||
/>
|
||||
<Flexbox align={'center'} gap={4} horizontal>
|
||||
<Switch
|
||||
checked={unlimited}
|
||||
onChange={(checked) => {
|
||||
updateAgentConfig({ enableHistoryCount: !checked });
|
||||
}}
|
||||
size={'small'}
|
||||
/>
|
||||
{t('settingChat.enableHistoryCount.alias')}
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
}
|
||||
placement={'top'}
|
||||
trigger={'click'}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={unlimited ? TimerOff : Timer}
|
||||
placement={'bottom'}
|
||||
title={t(
|
||||
unlimited
|
||||
? 'settingChat.enableHistoryCount.unlimited'
|
||||
: 'settingChat.enableHistoryCount.limited',
|
||||
{ number: historyCount || 0 },
|
||||
)}
|
||||
/>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default ActionLeft;
|
||||
@@ -1,48 +0,0 @@
|
||||
import { ActionIcon } from '@lobehub/ui';
|
||||
import { Popconfirm } from 'antd';
|
||||
import { useResponsive } from 'antd-style';
|
||||
import { Eraser } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import HotKeys from '@/components/HotKeys';
|
||||
import { CLEAN_MESSAGE_KEY, PREFIX_KEY } from '@/const/hotkeys';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
|
||||
import SaveTopic from '../Topic';
|
||||
|
||||
const ActionsRight = memo(() => {
|
||||
const { t } = useTranslation('setting');
|
||||
const [clearMessage] = useSessionStore((s) => [s.clearMessage, s.updateAgentConfig]);
|
||||
const { mobile } = useResponsive();
|
||||
const hotkeys = [PREFIX_KEY, CLEAN_MESSAGE_KEY].join('+');
|
||||
|
||||
useHotkeys(hotkeys, clearMessage, {
|
||||
preventDefault: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{mobile && <SaveTopic />}
|
||||
<Popconfirm
|
||||
cancelText={t('cancel', { ns: 'common' })}
|
||||
okButtonProps={{ danger: true }}
|
||||
okText={t('ok', { ns: 'common' })}
|
||||
onConfirm={() => clearMessage()}
|
||||
placement={'topRight'}
|
||||
title={t('confirmClearCurrentMessages', { ns: 'chat' })}
|
||||
>
|
||||
<ActionIcon
|
||||
icon={Eraser}
|
||||
placement={'bottom'}
|
||||
title={
|
||||
(<HotKeys desc={t('clearCurrentMessages', { ns: 'chat' })} keys={hotkeys} />) as any
|
||||
}
|
||||
/>
|
||||
</Popconfirm>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default ActionsRight;
|
||||
@@ -1,71 +0,0 @@
|
||||
import { ChatInputArea } from '@lobehub/ui';
|
||||
import { useResponsive } from 'antd-style';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { ReactNode, Suspense, memo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { CHAT_TEXTAREA_HEIGHT } from '@/const/layoutTokens';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { agentSelectors } from '@/store/session/selectors';
|
||||
|
||||
import ActionLeft from './ActionBar/ActionLeft';
|
||||
import ActionsRight from './ActionBar/ActionRight';
|
||||
|
||||
const Token = dynamic(() => import('./ActionBar/Token'), { ssr: false });
|
||||
|
||||
interface ChatContentProps {
|
||||
expand?: boolean;
|
||||
footer?: ReactNode;
|
||||
mobile?: boolean;
|
||||
onExpandChange?: (expand: boolean) => void;
|
||||
}
|
||||
|
||||
const ChatInputContent = memo<ChatContentProps>(
|
||||
({ expand, onExpandChange, mobile: defaultMobile, footer }) => {
|
||||
const { t } = useTranslation('common');
|
||||
|
||||
const [message, setMessage] = useState('');
|
||||
const { mobile: runtimeMobile } = useResponsive();
|
||||
|
||||
const mobile = runtimeMobile || defaultMobile;
|
||||
|
||||
const [isLoading, sendMessage, stopGenerateMessage, showTokenTag] = useSessionStore((s) => [
|
||||
!!s.chatLoadingId,
|
||||
s.sendMessage,
|
||||
s.stopGenerateMessage,
|
||||
agentSelectors.showTokenTag(s),
|
||||
]);
|
||||
|
||||
return (
|
||||
<ChatInputArea
|
||||
actions={
|
||||
<>
|
||||
<ActionLeft />
|
||||
{showTokenTag && (
|
||||
<Suspense>
|
||||
<Token input={message} />
|
||||
</Suspense>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
actionsRight={<ActionsRight />}
|
||||
expand={expand}
|
||||
footer={footer}
|
||||
loading={isLoading}
|
||||
minHeight={mobile ? 0 : CHAT_TEXTAREA_HEIGHT}
|
||||
onExpandChange={onExpandChange}
|
||||
onInputChange={setMessage}
|
||||
onSend={sendMessage}
|
||||
onStop={stopGenerateMessage}
|
||||
placeholder={t('sendPlaceholder', { ns: 'chat' })}
|
||||
text={{
|
||||
send: t('send'),
|
||||
stop: t('stop'),
|
||||
}}
|
||||
value={message}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default ChatInputContent;
|
||||
@@ -68,12 +68,13 @@ export interface ChatMessageAction {
|
||||
*/
|
||||
sendMessage: (text: string) => Promise<void>;
|
||||
stopGenerateMessage: () => void;
|
||||
|
||||
toggleChatLoading: (
|
||||
loading: boolean,
|
||||
id?: string,
|
||||
action?: string,
|
||||
) => AbortController | undefined;
|
||||
|
||||
updateInputMessage: (message: string) => void;
|
||||
}
|
||||
|
||||
export const chatMessage: StateCreator<
|
||||
@@ -94,7 +95,6 @@ export const chatMessage: StateCreator<
|
||||
// after remove topic , go back to default topic
|
||||
toggleTopic();
|
||||
},
|
||||
|
||||
coreProcessMessage: async (messages, userMessageId) => {
|
||||
const { dispatchMessage, fetchAIChatMessage, triggerFunctionCall, activeTopicId } = get();
|
||||
|
||||
@@ -163,6 +163,7 @@ export const chatMessage: StateCreator<
|
||||
deleteMessage: (id) => {
|
||||
get().dispatchMessage({ id, type: 'deleteMessage' });
|
||||
},
|
||||
|
||||
dispatchMessage: (payload) => {
|
||||
const { activeId } = get();
|
||||
const session = sessionSelectors.currentSession(get());
|
||||
@@ -172,7 +173,6 @@ export const chatMessage: StateCreator<
|
||||
|
||||
get().dispatchSession({ chats, id: activeId, type: 'updateSessionChat' });
|
||||
},
|
||||
|
||||
fetchAIChatMessage: async (messages, assistantId) => {
|
||||
const { dispatchMessage, toggleChatLoading } = get();
|
||||
|
||||
@@ -341,6 +341,7 @@ export const chatMessage: StateCreator<
|
||||
|
||||
toggleChatLoading(false);
|
||||
},
|
||||
|
||||
toggleChatLoading: (loading, id, action) => {
|
||||
if (loading) {
|
||||
const abortController = new AbortController();
|
||||
@@ -350,4 +351,7 @@ export const chatMessage: StateCreator<
|
||||
set({ abortController: undefined, chatLoadingId: undefined }, false, action);
|
||||
}
|
||||
},
|
||||
updateInputMessage: (message) => {
|
||||
set({ inputMessage: message }, false, t('updateInputMessage'));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2,9 +2,12 @@ export interface ChatState {
|
||||
abortController?: AbortController;
|
||||
activeTopicId?: string;
|
||||
chatLoadingId?: string;
|
||||
inputMessage: string;
|
||||
renameTopicId?: string;
|
||||
shareLoading?: boolean;
|
||||
topicLoadingId?: string;
|
||||
}
|
||||
|
||||
export const initialChatState: ChatState = {};
|
||||
export const initialChatState: ChatState = {
|
||||
inputMessage: '',
|
||||
};
|
||||
|
||||
@@ -41,6 +41,7 @@ export const currentChats = (s: SessionStore): ChatMessage[] => {
|
||||
return getChatsById(s.activeId)(s);
|
||||
};
|
||||
|
||||
const initTime = Date.now();
|
||||
// 针对新助手添加初始化时的自定义消息
|
||||
export const currentChatsWithGuideMessage = (s: SessionStore): ChatMessage[] => {
|
||||
const data = currentChats(s);
|
||||
@@ -67,14 +68,14 @@ export const currentChatsWithGuideMessage = (s: SessionStore): ChatMessage[] =>
|
||||
|
||||
const emptyInboxGuideMessage = {
|
||||
content: isInbox ? inboxMsg : !!meta.description ? agentSystemRoleMsg : agentMsg,
|
||||
createAt: Date.now(),
|
||||
createAt: initTime,
|
||||
extra: {},
|
||||
id: 'default',
|
||||
meta: meta || {
|
||||
avatar: DEFAULT_INBOX_AVATAR,
|
||||
},
|
||||
role: 'assistant',
|
||||
updateAt: Date.now(),
|
||||
updateAt: initTime,
|
||||
} as ChatMessage;
|
||||
|
||||
return [emptyInboxGuideMessage];
|
||||
|
||||
Reference in New Issue
Block a user