feat: upload audio file in chat
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const AudioWrapper = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AudioElement: React.FC<any> = (props) => {
|
||||||
|
return (
|
||||||
|
<AudioWrapper>
|
||||||
|
<audio {...props}></audio>
|
||||||
|
</AudioWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AudioElement;
|
||||||
@@ -90,7 +90,7 @@ export default {
|
|||||||
'playground.audio.stoprecord': 'Stop Recording',
|
'playground.audio.stoprecord': 'Stop Recording',
|
||||||
'playground.audio.generating.tips': 'Generated text will appear here.',
|
'playground.audio.generating.tips': 'Generated text will appear here.',
|
||||||
'playground.audio.uploadfile.tips':
|
'playground.audio.uploadfile.tips':
|
||||||
'Please upload an audio file, supported formats: {formats}',
|
'Upload an audio file, supported formats: {formats}',
|
||||||
'playground.input.multiplePaste': 'Batch Input Mode',
|
'playground.input.multiplePaste': 'Batch Input Mode',
|
||||||
'playground.input.multiplePaste.tips':
|
'playground.input.multiplePaste.tips':
|
||||||
'When enabled, pasted multi-line text will be automatically split by newline into separate entries in the form.',
|
'When enabled, pasted multi-line text will be automatically split by newline into separate entries in the form.',
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default {
|
|||||||
'playground.audio.startrecord': '开始录音',
|
'playground.audio.startrecord': '开始录音',
|
||||||
'playground.audio.stoprecord': '停止录音',
|
'playground.audio.stoprecord': '停止录音',
|
||||||
'playground.audio.generating.tips': '生成的文本将出现在这里',
|
'playground.audio.generating.tips': '生成的文本将出现在这里',
|
||||||
'playground.audio.uploadfile.tips': '请上传音频文件,支持格式:{formats}',
|
'playground.audio.uploadfile.tips': '上传音频文件,支持格式:{formats}',
|
||||||
'playground.audio.button.generate': '生成文本',
|
'playground.audio.button.generate': '生成文本',
|
||||||
'playground.input.multiplePaste': '批量输入',
|
'playground.input.multiplePaste': '批量输入',
|
||||||
'playground.input.multiplePaste.tips':
|
'playground.input.multiplePaste.tips':
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
|
import AudioElement from '@/components/audio-player/audio-element';
|
||||||
import IconFont from '@/components/icon-font';
|
import IconFont from '@/components/icon-font';
|
||||||
|
import UploadAudio from '@/components/upload-audio';
|
||||||
import HotKeys, { KeyMap } from '@/config/hotkeys';
|
import HotKeys, { KeyMap } from '@/config/hotkeys';
|
||||||
import { convertFileToBase64 } from '@/utils/load-audio-file';
|
import { convertFileToBase64, readAudioFile } from '@/utils/load-audio-file';
|
||||||
import { ClearOutlined, SendOutlined, SwapOutlined } from '@ant-design/icons';
|
import {
|
||||||
|
ClearOutlined,
|
||||||
|
CustomerServiceOutlined,
|
||||||
|
SendOutlined,
|
||||||
|
SwapOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
import { useIntl } from '@umijs/max';
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Checkbox, Divider, Input, Tooltip } from 'antd';
|
import { Button, Checkbox, Divider, Input, Tooltip } from 'antd';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@@ -14,8 +21,9 @@ import React, {
|
|||||||
useState
|
useState
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import styled from 'styled-components';
|
||||||
import { Roles } from '../config';
|
import { Roles } from '../config';
|
||||||
import { MessageItem } from '../config/types';
|
import { AudioFormat, MessageItem } from '../config/types';
|
||||||
import '../style/message-input.less';
|
import '../style/message-input.less';
|
||||||
import ThumbImg from './thumb-img';
|
import ThumbImg from './thumb-img';
|
||||||
import UploadImg from './upload-img';
|
import UploadImg from './upload-img';
|
||||||
@@ -26,6 +34,12 @@ const audioTypeMap: Record<string, string> = {
|
|||||||
'audio/mpeg': 'mp3'
|
'audio/mpeg': 'mp3'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AudioWrapper = styled.div`
|
||||||
|
audio {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
type CurrentMessage = Omit<MessageItem, 'uid'>;
|
type CurrentMessage = Omit<MessageItem, 'uid'>;
|
||||||
|
|
||||||
type ActionType =
|
type ActionType =
|
||||||
@@ -139,9 +153,14 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
|
|||||||
content: '',
|
content: '',
|
||||||
imgs: []
|
imgs: []
|
||||||
});
|
});
|
||||||
const imgCountRef = useRef(0);
|
const uidCountRef = useRef(0);
|
||||||
const inputRef = useRef<any>(null);
|
const inputRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const updateUidCount = () => {
|
||||||
|
uidCountRef.current += 1;
|
||||||
|
return uidCountRef.current;
|
||||||
|
};
|
||||||
|
|
||||||
const isDisabled = useMemo(() => {
|
const isDisabled = useMemo(() => {
|
||||||
return disabled
|
return disabled
|
||||||
? true
|
? true
|
||||||
@@ -239,9 +258,9 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
|
|||||||
|
|
||||||
if (imgs.length) {
|
if (imgs.length) {
|
||||||
const list = _.map(imgs, (img: string) => {
|
const list = _.map(imgs, (img: string) => {
|
||||||
imgCountRef.current += 1;
|
updateUidCount();
|
||||||
return {
|
return {
|
||||||
uid: imgCountRef.current,
|
uid: uidCountRef.current,
|
||||||
dataUrl: img
|
dataUrl: img
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -273,14 +292,19 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
|
|||||||
}) => {
|
}) => {
|
||||||
// convert audio file to base64
|
// convert audio file to base64
|
||||||
try {
|
try {
|
||||||
console.log('audio file====', data.file);
|
|
||||||
const base64Audio = await convertFileToBase64(data.file);
|
const base64Audio = await convertFileToBase64(data.file);
|
||||||
|
const audioData = await readAudioFile(data.file);
|
||||||
|
console.log('audioData====', audioData);
|
||||||
setMessage({
|
setMessage({
|
||||||
...message,
|
...message,
|
||||||
audio: {
|
audio: [
|
||||||
format: audioTypeMap[data.file.type],
|
{
|
||||||
dataUrl: base64Audio
|
uid: updateUidCount(),
|
||||||
}
|
format: audioTypeMap[data.file.type] as AudioFormat,
|
||||||
|
base64: base64Audio.split(',')[1],
|
||||||
|
data: _.pick(audioData, ['url', 'name', 'duration'])
|
||||||
|
}
|
||||||
|
]
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error converting audio to Base64:', error);
|
console.error('Error converting audio to Base64:', error);
|
||||||
@@ -298,6 +322,17 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteAudio = (uid: number | string) => {
|
||||||
|
const list = _.filter(
|
||||||
|
message.audio,
|
||||||
|
(item: MessageItem) => item.uid !== uid
|
||||||
|
);
|
||||||
|
setMessage({
|
||||||
|
...message,
|
||||||
|
audio: list
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleOnPaste = (e: any) => {
|
const handleOnPaste = (e: any) => {
|
||||||
const text = e.clipboardData.getData('text');
|
const text = e.clipboardData.getData('text');
|
||||||
if (!text) {
|
if (!text) {
|
||||||
@@ -318,16 +353,6 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteLastImage = useCallback(() => {
|
|
||||||
if (message.imgs && message.imgs?.length > 0) {
|
|
||||||
const newImgList = [...(message.imgs || [])];
|
|
||||||
const lastImage = newImgList.pop();
|
|
||||||
if (lastImage) {
|
|
||||||
handleDeleteImg(lastImage.uid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [message.imgs, handleDeleteImg]);
|
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
handleInputChange: handleInputChange
|
handleInputChange: handleInputChange
|
||||||
}));
|
}));
|
||||||
@@ -387,7 +412,7 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
|
|||||||
size="middle"
|
size="middle"
|
||||||
></UploadImg>
|
></UploadImg>
|
||||||
)}
|
)}
|
||||||
{/* {actions.includes('upload') && message.role === Roles.User && (
|
{actions.includes('upload') && message.role === Roles.User && (
|
||||||
<UploadAudio
|
<UploadAudio
|
||||||
type="text"
|
type="text"
|
||||||
accept={'.mp3,.wav'}
|
accept={'.mp3,.wav'}
|
||||||
@@ -396,12 +421,6 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
|
|||||||
icon={<CustomerServiceOutlined />}
|
icon={<CustomerServiceOutlined />}
|
||||||
onChange={handleUploadAudioChange}
|
onChange={handleUploadAudioChange}
|
||||||
></UploadAudio>
|
></UploadAudio>
|
||||||
)} */}
|
|
||||||
{actions.includes('upload') && message.role === Roles.User && (
|
|
||||||
<UploadImg
|
|
||||||
handleUpdateImgList={handleUpdateImgList}
|
|
||||||
size="middle"
|
|
||||||
></UploadImg>
|
|
||||||
)}
|
)}
|
||||||
{tools}
|
{tools}
|
||||||
{actions.includes('clear') && (
|
{actions.includes('clear') && (
|
||||||
@@ -490,11 +509,22 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ThumbImg
|
<div className="flex">
|
||||||
dataList={message.imgs || []}
|
<ThumbImg
|
||||||
onDelete={handleDeleteImg}
|
dataList={message.imgs || []}
|
||||||
editable={true}
|
onDelete={handleDeleteImg}
|
||||||
></ThumbImg>
|
editable={true}
|
||||||
|
></ThumbImg>
|
||||||
|
{message.audio && message.audio.length > 0 && (
|
||||||
|
<AudioWrapper>
|
||||||
|
<AudioElement
|
||||||
|
src={message.audio?.[0].data?.url}
|
||||||
|
onDelete={handleDeleteAudio}
|
||||||
|
controls
|
||||||
|
></AudioElement>
|
||||||
|
</AudioWrapper>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="input-box">
|
<div className="input-box">
|
||||||
{actions.includes('paste') ? (
|
{actions.includes('paste') ? (
|
||||||
<TextArea
|
<TextArea
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import AudioElement from '@/components/audio-player/audio-element';
|
||||||
import FullMarkdown from '@/components/markdown-viewer/full-markdown';
|
import FullMarkdown from '@/components/markdown-viewer/full-markdown';
|
||||||
import { Input } from 'antd';
|
import { Input } from 'antd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
@@ -8,11 +9,19 @@ import React, {
|
|||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useRef
|
useRef
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import styled from 'styled-components';
|
||||||
import { Roles } from '../../config';
|
import { Roles } from '../../config';
|
||||||
import { MessageItem, MessageItemAction } from '../../config/types';
|
import { MessageItem, MessageItemAction } from '../../config/types';
|
||||||
import ThumbImg from '../thumb-img';
|
import ThumbImg from '../thumb-img';
|
||||||
import ThinkContent from './think-content';
|
import ThinkContent from './think-content';
|
||||||
|
|
||||||
|
const AudioWrapper = styled.div`
|
||||||
|
padding-left: 10px;
|
||||||
|
audio {
|
||||||
|
padding-top: 10px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
interface MessageBodyProps {
|
interface MessageBodyProps {
|
||||||
ref?: any;
|
ref?: any;
|
||||||
data: MessageItem;
|
data: MessageItem;
|
||||||
@@ -93,6 +102,7 @@ const MessageBody: React.FC<MessageBodyProps> = forwardRef(
|
|||||||
role: data.role,
|
role: data.role,
|
||||||
content: data.content,
|
content: data.content,
|
||||||
uid: data.uid,
|
uid: data.uid,
|
||||||
|
audio: data.audio || [],
|
||||||
imgs: [...(data.imgs || []), ...list]
|
imgs: [...(data.imgs || []), ...list]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -117,6 +127,7 @@ const MessageBody: React.FC<MessageBodyProps> = forwardRef(
|
|||||||
role: data.role,
|
role: data.role,
|
||||||
content: data.content,
|
content: data.content,
|
||||||
uid: data.uid,
|
uid: data.uid,
|
||||||
|
audio: data.audio || [],
|
||||||
imgs: list
|
imgs: list
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -124,6 +135,7 @@ const MessageBody: React.FC<MessageBodyProps> = forwardRef(
|
|||||||
const handleMessageChange = (e: any) => {
|
const handleMessageChange = (e: any) => {
|
||||||
updateMessage?.({
|
updateMessage?.({
|
||||||
imgs: data.imgs || [],
|
imgs: data.imgs || [],
|
||||||
|
audio: data.audio || [],
|
||||||
role: data.role,
|
role: data.role,
|
||||||
content: e.target.value,
|
content: e.target.value,
|
||||||
uid: data.uid
|
uid: data.uid
|
||||||
@@ -143,6 +155,7 @@ const MessageBody: React.FC<MessageBodyProps> = forwardRef(
|
|||||||
role: data.role,
|
role: data.role,
|
||||||
content: editContent,
|
content: editContent,
|
||||||
uid: data.uid,
|
uid: data.uid,
|
||||||
|
audio: data.audio || [],
|
||||||
imgs: data.imgs
|
imgs: data.imgs
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -182,14 +195,25 @@ const MessageBody: React.FC<MessageBodyProps> = forwardRef(
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames('content-item-content', {
|
className={classNames('content-item-content', {
|
||||||
'has-img': data.imgs?.length
|
'has-img':
|
||||||
|
data.imgs?.length || (data.audio && data.audio?.length > 0)
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<ThumbImg
|
<div className="justify-start">
|
||||||
editable={editable}
|
<ThumbImg
|
||||||
dataList={data.imgs || []}
|
editable={editable}
|
||||||
onDelete={handleDeleteImg}
|
dataList={data.imgs || []}
|
||||||
/>
|
onDelete={handleDeleteImg}
|
||||||
|
/>
|
||||||
|
{data.audio && data.audio.length > 0 && (
|
||||||
|
<AudioWrapper>
|
||||||
|
<AudioElement
|
||||||
|
src={data.audio?.[0]?.data.url}
|
||||||
|
controls
|
||||||
|
></AudioElement>
|
||||||
|
</AudioWrapper>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{data.content && <div className="text">{data.content}</div>}
|
{data.content && <div className="text">{data.content}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -199,15 +223,26 @@ const MessageBody: React.FC<MessageBodyProps> = forwardRef(
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames('message-content-input', {
|
className={classNames('message-content-input', {
|
||||||
'has-img': data.imgs?.length
|
'has-img':
|
||||||
|
data.imgs?.length || (data.audio && data.audio?.length > 0)
|
||||||
})}
|
})}
|
||||||
onClick={handleClickWrapper}
|
onClick={handleClickWrapper}
|
||||||
>
|
>
|
||||||
<ThumbImg
|
<div className="justify-start">
|
||||||
editable={editable}
|
<ThumbImg
|
||||||
dataList={data.imgs || []}
|
editable={editable}
|
||||||
onDelete={handleDeleteImg}
|
dataList={data.imgs || []}
|
||||||
/>
|
onDelete={handleDeleteImg}
|
||||||
|
/>
|
||||||
|
{data.audio && data.audio.length > 0 && (
|
||||||
|
<AudioWrapper>
|
||||||
|
<AudioElement
|
||||||
|
src={data.audio?.[0]?.data.url}
|
||||||
|
controls
|
||||||
|
></AudioElement>
|
||||||
|
</AudioWrapper>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<>
|
<>
|
||||||
{data.role === Roles.User ? (
|
{data.role === Roles.User ? (
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import _, { map } from 'lodash';
|
import _, { map } from 'lodash';
|
||||||
import { CREAT_IMAGE_API } from '../apis';
|
import { CREAT_IMAGE_API } from '../apis';
|
||||||
import { MessageItem } from './types';
|
import { AudioData, MessageItem } from './types';
|
||||||
|
|
||||||
export const Roles = {
|
export const Roles = {
|
||||||
User: 'user',
|
User: 'user',
|
||||||
@@ -76,7 +76,33 @@ export const generateMessagesByListContent = (messageList: any[]) => {
|
|||||||
content: content
|
content: content
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return _.omit(item, ['uid', 'imgs']);
|
|
||||||
|
// audio
|
||||||
|
if (item.audio?.length) {
|
||||||
|
const content = map(item.audio, (item: AudioData) => {
|
||||||
|
return {
|
||||||
|
type: 'input_audio',
|
||||||
|
input_audio: {
|
||||||
|
data: item.base64,
|
||||||
|
format: item.format
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (item.content) {
|
||||||
|
content.push({
|
||||||
|
type: 'text',
|
||||||
|
text: item.content
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
role: item.role,
|
||||||
|
content: content
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.omit(item, ['uid', 'imgs', 'audio']);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,23 @@ export type MessageItemAction =
|
|||||||
| 'markdown'
|
| 'markdown'
|
||||||
| 'edit';
|
| 'edit';
|
||||||
|
|
||||||
|
export type AudioFormat = 'wav' | 'mp3';
|
||||||
|
|
||||||
|
export interface AudioData {
|
||||||
|
uid: string | number;
|
||||||
|
base64: string;
|
||||||
|
format: AudioFormat;
|
||||||
|
data: {
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
duration: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface MessageItem {
|
export interface MessageItem {
|
||||||
content: string;
|
content: string;
|
||||||
imgs?: { uid: string | number; dataUrl: string }[];
|
imgs?: { uid: string | number; dataUrl: string }[];
|
||||||
audio?: { uid: string | number; dataUrl: string; format: 'wav' | 'mp3' };
|
audio?: AudioData[];
|
||||||
role: string;
|
role: string;
|
||||||
title?: React.ReactNode;
|
title?: React.ReactNode;
|
||||||
uid: number;
|
uid: number;
|
||||||
|
|||||||
@@ -9,7 +9,16 @@ export const convertFileToBase64 = (file: File): Promise<string> => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadAudioData = async (data: any, type: string) => {
|
export const loadAudioData = async (
|
||||||
|
data: any,
|
||||||
|
type: string
|
||||||
|
): Promise<{
|
||||||
|
data: Blob;
|
||||||
|
size: number | string;
|
||||||
|
type: string;
|
||||||
|
duration: number;
|
||||||
|
url: string;
|
||||||
|
}> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const audioBlob = new Blob([data], { type: type });
|
const audioBlob = new Blob([data], { type: type });
|
||||||
@@ -40,7 +49,9 @@ export const loadAudioData = async (data: any, type: string) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const readAudioFile = async (file: File) => {
|
export const readAudioFile = async (
|
||||||
|
file: File
|
||||||
|
): Promise<{ url: string; name: string; duration: number }> => {
|
||||||
console.log('file====', file);
|
console.log('file====', file);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|||||||
Reference in New Issue
Block a user