style: upload image to vision model adapting to mobile device (#457)

*  style: 上传图片适配移动端
This commit is contained in:
Arvin Xu
2023-11-14 17:40:33 +08:00
committed by GitHub
parent 59e833b711
commit 9c4f4eebd8
9 changed files with 191 additions and 182 deletions
@@ -1,11 +1,7 @@
import { TextArea } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { memo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { memo } from 'react';
import { useSessionStore } from '@/store/session';
import { useSendMessage } from '../../../features/ChatInput/useSend';
import InputAreaInner from '@/app/chat/features/ChatInput/InputAreaInner';
const useStyles = createStyles(({ css }) => {
return {
@@ -22,46 +18,11 @@ const useStyles = createStyles(({ css }) => {
});
const InputArea = memo(() => {
const { t } = useTranslation('common');
const { cx, styles } = useStyles();
const isChineseInput = useRef(false);
const [loading, message, updateInputMessage] = useSessionStore((s) => [
!!s.chatLoadingId,
s.inputMessage,
s.updateInputMessage,
]);
const handleSend = useSendMessage();
const { styles } = useStyles();
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 className={styles.textareaContainer}>
<InputAreaInner className={styles.textarea} />
</div>
);
});
@@ -0,0 +1,17 @@
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import FileList from '@/app/chat/components/FileList';
import { useFileStore } from '@/store/files';
const Files = memo(() => {
const inputFilesList = useFileStore((s) => s.inputFilesList);
return (
<Flexbox padding={12}>
<FileList alwaysShowClose items={inputFilesList} />
</Flexbox>
);
});
export default Files;
@@ -1,79 +1,25 @@
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 { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import useControlledState from 'use-merge-value';
import ActionBar from '@/app/chat/features/ChatInput/ActionBar';
import InputAreaInner from '@/app/chat/features/ChatInput/InputAreaInner';
import SaveTopic from '@/app/chat/features/ChatInput/Topic';
import SendButton from './SendButton';
import { useStyles } from './style.mobile';
export type ChatInputAreaMobile = {
loading?: boolean;
onChange?: (value: string) => void;
onSend?: (value: string) => void;
onStop?: () => void;
value?: string;
};
const ChatInputArea = memo(() => {
const { cx, styles } = useStyles();
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>
return (
<Flexbox className={cx(styles.container)} gap={12}>
<ActionBar rightAreaStartRender={<SaveTopic />} />
<Flexbox className={styles.inner} gap={8} horizontal>
<InputAreaInner mobile />
<SendButton />
</Flexbox>
);
},
);
</Flexbox>
);
});
export default ChatInputArea;
@@ -0,0 +1,30 @@
import { Icon } from '@lobehub/ui';
import { Button } from 'antd';
import { Loader2, SendHorizonal } from 'lucide-react';
import { memo } from 'react';
import { useSendMessage } from '@/app/chat/features/ChatInput/useSend';
import { useSessionStore } from '@/store/session';
const SendButton = memo(() => {
const [loading, onStop] = useSessionStore((s) => [!!s.chatLoadingId, s.stopGenerateMessage]);
const handleSend = useSendMessage();
return loading ? (
<Button
icon={loading && <Icon icon={Loader2} spin />}
onClick={onStop}
style={{ flex: 'none' }}
/>
) : (
<Button
icon={<Icon icon={SendHorizonal} />}
onClick={handleSend}
style={{ flex: 'none' }}
type={'primary'}
/>
);
});
export default SendButton;
@@ -1,10 +1,10 @@
import { createStyles } from 'antd-style';
import { memo, useState } from 'react';
import { memo } from 'react';
import SafeSpacing from '@/components/SafeSpacing';
import { CHAT_TEXTAREA_HEIGHT_MOBILE } from '@/const/layoutTokens';
import { useSessionStore } from '@/store/session';
import Files from './Files';
import ChatInputArea from './Mobile';
const useStyles = createStyles(
@@ -23,25 +23,12 @@ 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 (
<>
<Files />
<SafeSpacing height={CHAT_TEXTAREA_HEIGHT_MOBILE} mobile position={'bottom'} />
<div className={styles}>
<ChatInputArea
loading={isLoading}
onChange={setMessage}
onSend={sendMessage}
onStop={stopGenerateMessage}
value={message}
/>
<ChatInputArea />
</div>
</>
);
@@ -28,6 +28,9 @@ export const useStyles = createStyles(({ css, cx, token, isDarkMode }) => {
`);
return {
alwaysShowClose: css`
opacity: 1 !important;
`,
closeIcon,
container: css`
cursor: pointer;
+56 -52
View File
@@ -10,60 +10,64 @@ import { useFileStore } from '@/store/files';
import { IMAGE_SIZE, useStyles } from './FileItem.style';
const FileItem = memo<{ editable: boolean; id: string; onClick: () => void }>(
({ editable, id, onClick }) => {
const { styles } = useStyles();
const [useFetchFile, removeFile] = useFileStore((s) => [s.useFetchFile, s.removeFile]);
interface FileItemProps {
alwaysShowClose?: boolean;
editable: boolean;
id: string;
onClick: () => void;
}
const FileItem = memo<FileItemProps>(({ editable, id, onClick, alwaysShowClose }) => {
const { styles, cx } = useStyles();
const [useFetchFile, removeFile] = useFileStore((s) => [s.useFetchFile, s.removeFile]);
const { data, isLoading } = useFetchFile(id);
const { data, isLoading } = useFetchFile(id);
return (
<Flexbox className={styles.container} onClick={onClick}>
{isLoading ? (
<Skeleton
active
title={{
style: { borderRadius: 8, height: IMAGE_SIZE },
width: IMAGE_SIZE,
}}
/>
) : (
<Flexbox className={styles.imageCtn}>
<div className={styles.imageWrapper}>
{data ? (
<Image
alt={data.name || ''}
className={styles.image}
fetchPriority={'high'}
height={IMAGE_SIZE}
loading={'lazy'}
src={data.url}
width={IMAGE_SIZE}
/>
) : (
<Center className={styles.notFound} height={'100%'}>
<Icon icon={LucideImageOff} size={{ fontSize: 28 }} />
</Center>
)}
</div>
</Flexbox>
)}
{/* only show close icon when editable */}
{editable && (
<Center
className={styles.closeIcon}
onClick={(e) => {
e.stopPropagation();
return (
<Flexbox className={styles.container} onClick={onClick}>
{isLoading ? (
<Skeleton
active
title={{
style: { borderRadius: 8, height: IMAGE_SIZE },
width: IMAGE_SIZE,
}}
/>
) : (
<Flexbox className={styles.imageCtn}>
<div className={styles.imageWrapper}>
{data ? (
<Image
alt={data.name || ''}
className={styles.image}
fetchPriority={'high'}
height={IMAGE_SIZE}
loading={'lazy'}
src={data.url}
width={IMAGE_SIZE}
/>
) : (
<Center className={styles.notFound} height={'100%'}>
<Icon icon={LucideImageOff} size={{ fontSize: 28 }} />
</Center>
)}
</div>
</Flexbox>
)}
{/* only show close icon when editable */}
{editable && (
<Center
className={cx(styles.closeIcon, alwaysShowClose && styles.alwaysShowClose)}
onClick={(e) => {
e.stopPropagation();
removeFile(id);
}}
>
<CloseCircleFilled />
</Center>
)}
</Flexbox>
);
},
);
removeFile(id);
}}
>
<CloseCircleFilled />
</Center>
)}
</Flexbox>
);
});
export default FileItem;
+5 -1
View File
@@ -14,11 +14,14 @@ import Lightbox from './Lightbox';
// }));
interface FileListProps {
alwaysShowClose?: boolean;
editable?: boolean;
items: string[];
}
const FileList = memo<FileListProps>(({ items, editable = true }) => {
const FileList = memo<FileListProps>(({ items, editable = true, alwaysShowClose }) => {
// const { styles } = useStyles();
const [showLightbox, setShowLightbox] = useState(false);
@@ -38,6 +41,7 @@ const FileList = memo<FileListProps>(({ items, editable = true }) => {
>
{items.map((i, index) => (
<FileItem
alwaysShowClose={alwaysShowClose}
editable={editable}
id={i}
key={i}
@@ -0,0 +1,57 @@
import { Input, TextArea } from '@lobehub/ui';
import { memo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useSessionStore } from '@/store/session';
import { useSendMessage } from '../useSend';
export interface InputAreaInnerProps {
className?: string;
mobile?: boolean;
}
const InputAreaInner = memo<InputAreaInnerProps>(({ className, mobile }) => {
const { t } = useTranslation('chat');
const isChineseInput = useRef(false);
const [loading, message, updateInputMessage] = useSessionStore((s) => [
!!s.chatLoadingId,
s.inputMessage,
s.updateInputMessage,
]);
const handleSend = useSendMessage();
const Render = mobile ? Input : TextArea;
return (
<Render
className={className}
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')}
resize={false}
type={mobile ? 'block' : 'pure'}
value={message}
/>
);
});
export default InputAreaInner;