Skip to content

Commit

Permalink
♻️ refactor: refactor the input area to suit the files upload feature…
Browse files Browse the repository at this point in the history
… (#442)

* ♻️ refactor: refactor the input area from ui

* ♻️ refactor: refactor ActionBar to a configurable stage
  • Loading branch information
arvinxx authored Nov 11, 2023
1 parent 240c0c3 commit 57a61fd
Show file tree
Hide file tree
Showing 22 changed files with 559 additions and 259 deletions.
40 changes: 34 additions & 6 deletions src/app/chat/(desktop)/features/ChatInput/Footer.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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>
);
});

Expand Down
69 changes: 69 additions & 0 deletions src/app/chat/(desktop)/features/ChatInput/InputArea.tsx
Original file line number Diff line number Diff line change
@@ -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;
41 changes: 37 additions & 4 deletions src/app/chat/(desktop)/features/ChatInput/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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>
);
});
Expand Down
17 changes: 17 additions & 0 deletions src/app/chat/(desktop)/features/ChatInput/useSend.ts
Original file line number Diff line number Diff line change
@@ -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('');
}, []);
};
79 changes: 79 additions & 0 deletions src/app/chat/(mobile)/features/ChatInput/Mobile.tsx
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -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`
Expand All @@ -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>
</>
);
Expand Down
19 changes: 19 additions & 0 deletions src/app/chat/(mobile)/features/ChatInput/style.mobile.ts
Original file line number Diff line number Diff line change
@@ -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;
`,
};
});
39 changes: 39 additions & 0 deletions src/app/chat/features/ChatInput/ActionBar/Clear.tsx
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 57a61fd

Please sign in to comment.