Skip to content

Commit

Permalink
✨ style: upload image to vision model adapting to mobile device (#457)
Browse files Browse the repository at this point in the history
* ✨ style: 上传图片适配移动端
  • Loading branch information
arvinxx authored Nov 14, 2023
1 parent 59e833b commit 9c4f4ee
Show file tree
Hide file tree
Showing 9 changed files with 191 additions and 182 deletions.
49 changes: 5 additions & 44 deletions src/app/chat/(desktop)/features/ChatInput/InputArea.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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>
);
});
Expand Down
17 changes: 17 additions & 0 deletions src/app/chat/(mobile)/features/ChatInput/Files.tsx
Original file line number Diff line number Diff line change
@@ -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;
82 changes: 14 additions & 68 deletions src/app/chat/(mobile)/features/ChatInput/Mobile.tsx
Original file line number Diff line number Diff line change
@@ -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;
30 changes: 30 additions & 0 deletions src/app/chat/(mobile)/features/ChatInput/SendButton.tsx
Original file line number Diff line number Diff line change
@@ -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;
21 changes: 4 additions & 17 deletions src/app/chat/(mobile)/features/ChatInput/index.tsx
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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>
</>
);
Expand Down
3 changes: 3 additions & 0 deletions src/app/chat/components/FileList/FileItem.style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ export const useStyles = createStyles(({ css, cx, token, isDarkMode }) => {
`);

return {
alwaysShowClose: css`
opacity: 1 !important;
`,
closeIcon,
container: css`
cursor: pointer;
Expand Down
108 changes: 56 additions & 52 deletions src/app/chat/components/FileList/FileItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
6 changes: 5 additions & 1 deletion src/app/chat/components/FileList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -38,6 +41,7 @@ const FileList = memo<FileListProps>(({ items, editable = true }) => {
>
{items.map((i, index) => (
<FileItem
alwaysShowClose={alwaysShowClose}
editable={editable}
id={i}
key={i}
Expand Down
Loading

0 comments on commit 9c4f4ee

Please sign in to comment.