Skip to content

Commit 70f3373

Browse files
authored
add image input (#486)
* add image input * use json
1 parent af16817 commit 70f3373

File tree

9 files changed

+181
-28
lines changed

9 files changed

+181
-28
lines changed

projects/app/public/locales/en/common.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,10 +233,11 @@
233233
},
234234
"chat": {
235235
"Audio Speech Error": "Audio Speech Error",
236-
"Speaking": "I'm listening...",
237236
"Record": "Speech",
238237
"Restart": "Restart",
238+
"Select File": "Select file",
239239
"Send Message": "Send Message",
240+
"Speaking": "I'm listening...",
240241
"Stop Speak": "Stop Speak",
241242
"Type a message": "Input problem",
242243
"tts": {
@@ -589,8 +590,8 @@
589590
"wallet": {
590591
"bill": {
591592
"Audio Speech": "Audio Speech",
592-
"bill username": "User",
593-
"Whisper": "Whisper"
593+
"Whisper": "Whisper",
594+
"bill username": "User"
594595
}
595596
}
596597
}

projects/app/public/locales/zh/common.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@
235235
"Audio Speech Error": "语音播报异常",
236236
"Record": "语音输入",
237237
"Restart": "重开对话",
238+
"Select File": "选择文件",
238239
"Send Message": "发送",
239240
"Speaking": "我在听,请说...",
240241
"Stop Speak": "停止录音",
@@ -589,8 +590,8 @@
589590
"wallet": {
590591
"bill": {
591592
"Audio Speech": "语音播报",
592-
"bill username": "用户",
593-
"Whisper": "语音输入"
593+
"Whisper": "语音输入",
594+
"bill username": "用户"
594595
}
595596
}
596597
}

projects/app/src/components/ChatBox/MessageInput.tsx

Lines changed: 140 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { useSpeech } from '@/web/common/hooks/useSpeech';
22
import { useSystemStore } from '@/web/common/system/useSystemStore';
3-
import { Box, Flex, Spinner, Textarea } from '@chakra-ui/react';
4-
import React, { useRef, useEffect } from 'react';
3+
import { Box, Flex, Image, Spinner, Textarea } from '@chakra-ui/react';
4+
import React, { useRef, useEffect, useCallback, useState, useMemo } from 'react';
55
import { useTranslation } from 'react-i18next';
66
import MyTooltip from '../MyTooltip';
77
import MyIcon from '../Icon';
88
import styles from './index.module.scss';
99
import { useRouter } from 'next/router';
10+
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
11+
import { compressImgAndUpload } from '@/web/common/file/controller';
12+
import { useToast } from '@/web/common/hooks/useToast';
1013

1114
const MessageInput = ({
1215
onChange,
@@ -38,6 +41,60 @@ const MessageInput = ({
3841
const { t } = useTranslation();
3942
const textareaMinH = '22px';
4043
const havInput = !!TextareaDom.current?.value;
44+
const { toast } = useToast();
45+
const [imgBase64Array, setImgBase64Array] = useState<string[]>([]);
46+
const [fileList, setFileList] = useState<File[]>([]);
47+
const [imgSrcArray, setImgSrcArray] = useState<string[]>([]);
48+
49+
const { File, onOpen: onOpenSelectFile } = useSelectFile({
50+
fileType: '.jpg,.png',
51+
multiple: true
52+
});
53+
54+
useEffect(() => {
55+
fileList.forEach((file) => {
56+
const reader = new FileReader();
57+
reader.readAsDataURL(file);
58+
reader.onload = async () => {
59+
setImgBase64Array((prev) => [...prev, reader.result as string]);
60+
};
61+
});
62+
}, [fileList]);
63+
64+
const onSelectFile = useCallback((e: File[]) => {
65+
if (!e || e.length === 0) {
66+
return;
67+
}
68+
setFileList(e);
69+
}, []);
70+
71+
const handleSend = useCallback(async () => {
72+
try {
73+
for (const file of fileList) {
74+
const src = await compressImgAndUpload({
75+
file,
76+
maxW: 1000,
77+
maxH: 1000,
78+
maxSize: 1024 * 1024 * 2
79+
});
80+
imgSrcArray.push(src);
81+
}
82+
} catch (err: any) {
83+
toast({
84+
title: typeof err === 'string' ? err : '文件上传异常',
85+
status: 'warning'
86+
});
87+
}
88+
89+
const textareaValue = TextareaDom.current?.value || '';
90+
const inputMessage =
91+
imgSrcArray.length === 0
92+
? textareaValue
93+
: `\`\`\`img-block\n${JSON.stringify(imgSrcArray)}\n\`\`\`\n${textareaValue}`;
94+
onSendMessage(inputMessage);
95+
setImgBase64Array([]);
96+
setImgSrcArray([]);
97+
}, [TextareaDom, fileList, imgSrcArray, onSendMessage, toast]);
4198

4299
useEffect(() => {
43100
if (!stream) {
@@ -60,7 +117,7 @@ const MessageInput = ({
60117
<>
61118
<Box m={['0 auto', '10px auto']} w={'100%'} maxW={['auto', 'min(800px, 100%)']} px={[0, 5]}>
62119
<Box
63-
py={'18px'}
120+
py={imgBase64Array.length > 0 ? '8px' : '18px'}
64121
position={'relative'}
65122
boxShadow={isSpeaking ? `0 0 10px rgba(54,111,255,0.4)` : `0 0 10px rgba(0,0,0,0.2)`}
66123
{...(isPc
@@ -93,11 +150,74 @@ const MessageInput = ({
93150
<Spinner size={'sm'} mr={4} />
94151
{t('chat.Converting to text')}
95152
</Box>
153+
{/* file uploader */}
154+
<Flex
155+
position={'absolute'}
156+
alignItems={'center'}
157+
left={['12px', '14px']}
158+
bottom={['15px', '13px']}
159+
h={['26px', '32px']}
160+
zIndex={10}
161+
cursor={'pointer'}
162+
onClick={onOpenSelectFile}
163+
>
164+
<MyTooltip label={t('core.chat.Select File')}>
165+
<MyIcon name={'core/chat/fileSelect'} />
166+
</MyTooltip>
167+
<File onSelect={onSelectFile} />
168+
</Flex>
169+
{/* file preview */}
170+
<Flex w={'96%'} wrap={'wrap'} ml={4}>
171+
{imgBase64Array.length > 0 &&
172+
imgBase64Array.map((src, index) => (
173+
<Box
174+
key={index}
175+
border={'1px solid rgba(0,0,0,0.12)'}
176+
mr={2}
177+
mb={2}
178+
rounded={'md'}
179+
position={'relative'}
180+
_hover={{
181+
'.close-icon': { display: 'block' }
182+
}}
183+
>
184+
<MyIcon
185+
name={'closeSolid'}
186+
w={'16px'}
187+
h={'16px'}
188+
color={'myGray.700'}
189+
cursor={'pointer'}
190+
_hover={{ color: 'myBlue.600' }}
191+
position={'absolute'}
192+
right={-2}
193+
top={-2}
194+
onClick={() => {
195+
setImgBase64Array((prev) => {
196+
prev.splice(index, 1);
197+
return [...prev];
198+
});
199+
}}
200+
className="close-icon"
201+
display={['', 'none']}
202+
/>
203+
<Image
204+
alt={'img'}
205+
src={src}
206+
w={'80px'}
207+
h={'80px'}
208+
rounded={'md'}
209+
objectFit={'cover'}
210+
/>
211+
</Box>
212+
))}
213+
</Flex>
96214
{/* input area */}
97215
<Textarea
98216
ref={TextareaDom}
99217
py={0}
100218
pr={['45px', '55px']}
219+
pl={['36px', '40px']}
220+
mt={imgBase64Array.length > 0 ? 4 : 0}
101221
border={'none'}
102222
_focusVisible={{
103223
border: 'none'
@@ -124,13 +244,28 @@ const MessageInput = ({
124244
onKeyDown={(e) => {
125245
// enter send.(pc or iframe && enter and unPress shift)
126246
if ((isPc || window !== parent) && e.keyCode === 13 && !e.shiftKey) {
127-
onSendMessage(TextareaDom.current?.value || '');
247+
handleSend();
128248
e.preventDefault();
129249
}
130250
// 全选内容
131251
// @ts-ignore
132252
e.key === 'a' && e.ctrlKey && e.target?.select();
133253
}}
254+
onPaste={(e) => {
255+
const clipboardData = e.clipboardData;
256+
if (clipboardData) {
257+
const items = clipboardData.items;
258+
const files: File[] = [];
259+
for (let i = 0; i < items.length; i++) {
260+
const item = items[i];
261+
if (item.kind === 'file') {
262+
const file = item.getAsFile();
263+
files.push(file as File);
264+
}
265+
}
266+
setFileList(files);
267+
}
268+
}}
134269
/>
135270
<Flex
136271
position={'absolute'}
@@ -195,7 +330,7 @@ const MessageInput = ({
195330
return onStop();
196331
}
197332
if (havInput) {
198-
onSendMessage(TextareaDom.current?.value || '');
333+
return handleSend();
199334
}
200335
}}
201336
>

projects/app/src/components/ChatBox/index.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,7 @@ import { useToast } from '@/web/common/hooks/useToast';
1717
import { useAudioPlay } from '@/web/common/utils/voice';
1818
import { getErrText } from '@fastgpt/global/common/error/utils';
1919
import { useCopyData } from '@/web/common/hooks/useCopyData';
20-
import {
21-
Box,
22-
Card,
23-
Flex,
24-
Input,
25-
Textarea,
26-
Button,
27-
useTheme,
28-
BoxProps,
29-
FlexProps,
30-
Spinner
31-
} from '@chakra-ui/react';
20+
import { Box, Card, Flex, Input, Button, useTheme, BoxProps, FlexProps } from '@chakra-ui/react';
3221
import { feConfigs } from '@/web/common/system/staticData';
3322
import { eventBus } from '@/web/common/utils/eventbus';
3423
import { adaptChat2GptMessages } from '@fastgpt/global/core/chat/adapt';
@@ -633,7 +622,7 @@ const ChatBox = (
633622
borderRadius={'8px 0 8px 8px'}
634623
textAlign={'left'}
635624
>
636-
<Box as={'p'}>{item.value}</Box>
625+
<Markdown source={item.value} isChatting={false} />
637626
</Card>
638627
</Box>
639628
</>
Lines changed: 3 additions & 0 deletions
Loading

projects/app/src/components/Icon/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ const iconPaths = {
112112
'core/chat/recordFill': () => import('./icons/core/chat/recordFill.svg'),
113113
'core/chat/stopSpeechFill': () => import('./icons/core/chat/stopSpeechFill.svg'),
114114
'core/chat/stopSpeech': () => import('./icons/core/chat/stopSpeech.svg'),
115-
'core/chat/speaking': () => import('./icons/core/chat/speaking.svg')
115+
'core/chat/speaking': () => import('./icons/core/chat/speaking.svg'),
116+
'core/chat/fileSelect': () => import('./icons/core/chat/fileSelect.svg')
116117
};
117118

118119
export type IconName = keyof typeof iconPaths;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Box, Flex } from '@chakra-ui/react';
2+
import MdImage from '../img/Image';
3+
4+
const ImageBlock = ({ images }: { images: string }) => {
5+
return (
6+
<Flex w={'100%'} wrap={'wrap'}>
7+
{JSON.parse(images).map((src: string) => {
8+
return (
9+
<Box key={src} mr={2} mb={2} rounded={'md'} flex={'0 0 auto'} w={'100px'} h={'100px'}>
10+
<MdImage src={src} />
11+
</Box>
12+
);
13+
})}
14+
</Flex>
15+
);
16+
};
17+
18+
export default ImageBlock;

projects/app/src/components/Markdown/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ const MdImage = dynamic(() => import('./img/Image'));
1616
const ChatGuide = dynamic(() => import('./chat/Guide'));
1717
const EChartsCodeBlock = dynamic(() => import('./img/EChartsCodeBlock'));
1818
const QuoteBlock = dynamic(() => import('./chat/Quote'));
19+
const ImageBlock = dynamic(() => import('./chat/Image'));
1920

2021
export enum CodeClassName {
2122
guide = 'guide',
2223
mermaid = 'mermaid',
2324
echarts = 'echarts',
24-
quote = 'quote'
25+
quote = 'quote',
26+
img = 'img'
2527
}
2628

2729
function Code({ inline, className, children }: any) {
@@ -41,6 +43,9 @@ function Code({ inline, className, children }: any) {
4143
if (codeType === CodeClassName.quote) {
4244
return <QuoteBlock code={String(children)} />;
4345
}
46+
if (codeType === CodeClassName.img) {
47+
return <ImageBlock images={String(children)} />;
48+
}
4449
return (
4550
<CodeLight className={className} inline={inline} match={match}>
4651
{children}

projects/app/src/web/common/hooks/useSpeech.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const useSpeech = (props?: { shareId?: string }) => {
1212
const { toast } = useToast();
1313
const [isSpeaking, setIsSpeaking] = useState(false);
1414
const [isTransCription, setIsTransCription] = useState(false);
15-
const [audioSecond, setAudioSecone] = useState(0);
15+
const [audioSecond, setAudioSecond] = useState(0);
1616
const intervalRef = useRef<any>();
1717
const startTimestamp = useRef(0);
1818

@@ -59,11 +59,11 @@ export const useSpeech = (props?: { shareId?: string }) => {
5959

6060
mediaRecorder.current.onstart = () => {
6161
startTimestamp.current = Date.now();
62-
setAudioSecone(0);
62+
setAudioSecond(0);
6363
intervalRef.current = setInterval(() => {
6464
const currentTimestamp = Date.now();
6565
const duration = (currentTimestamp - startTimestamp.current) / 1000;
66-
setAudioSecone(duration);
66+
setAudioSecond(duration);
6767
}, 1000);
6868
};
6969

0 commit comments

Comments
 (0)