style: upload image in message input

main
jialin 1 year ago
parent a5cebb88ef
commit af077c4706

@ -2,7 +2,7 @@ import { platformCall } from '@/utils';
const platform = platformCall();
const KeybindingsMap = {
CREATE: ['alt+ctrl+N', 'alt+meta+N'],
CLEAR: ['alt+ctrl+W', 'alt+meta+W'],
CLEAR: ['alt+ctrl+K', 'alt+meta+K'],
RIGHT: ['ctrl+RIGHT', 'meta+RIGHT'],
SAVE: ['ctrl+S', 'meta+S'],
SUBMIT: ['ctrl+enter', 'meta+enter'],
@ -36,7 +36,7 @@ const KeybiningList: KeybindingValue[] = Object.entries(KeybindingsMap).map(
keybinding: keybinding,
command: key,
textKeybinding: platform.isMac
? keybinding.replace('meta', 'Command').replace('alt', 'Option')
? keybinding.replace('meta', 'Cmd').replace('alt', 'Option')
: keybinding.replace('ctrl', 'Ctrl'),
iconKeybinding: platform.isMac
? keybinding.replace('meta', '⌘').replace('alt', '⌥')

@ -29,5 +29,9 @@ export default {
'A stop sequence is a predefined or user-specified text string that signals the AI to stop generating further tokens when these sequences appear.',
'playground.viewcode.tips':
'Your API Key can be found {here}.You should use environment variables or a secret management tool to expose your key to your applications.',
'playground.viewcode.here': 'here'
'playground.viewcode.here': 'here',
'playground.delete.img': 'Delete Image',
'playground.img.upload': 'Upload Image',
'playground.img.upload.success': 'Upload Success',
'playground.img.upload.error': 'Upload Error'
};

@ -29,5 +29,9 @@ export default {
'停止序列是一个预定义或用户指定的文本字符串,当这些序列出现时,它会提示 AI 停止生成后续的 token。',
'playground.viewcode.tips':
'{here} 查看 API 密钥。您应该使用环境变量或密钥管理工具将您的密钥暴露给您的应用程序。',
'playground.viewcode.here': '这里'
'playground.viewcode.here': '这里',
'playground.delete.img': '删除图片',
'playground.img.upload': '上传图片',
'playground.img.upload.success': '上传成功',
'playground.img.upload.error': '上传失败'
};

@ -152,7 +152,7 @@ export async function queryHuggingfaceModels(
...params,
...options,
limit: 100,
additionalFields: ['sha'],
additionalFields: ['sha', 'tags'],
fetch(url: string, config: any) {
try {
const newUrl = params.search.sort

@ -1,14 +1,16 @@
import CopyButton from '@/components/copy-button';
import HotKeys from '@/config/hotkeys';
import { MinusCircleOutlined } from '@ant-design/icons';
import { CloseOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Input, Space, Tooltip } from 'antd';
import classNames from 'classnames';
import _ from 'lodash';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import { Roles } from '../config';
import '../style/message-item.less';
import ThumbImg from './thumb-img';
import UploadImg from './upload-img';
interface MessageItemProps {
role: string;
content: string;
@ -25,12 +27,10 @@ const MessageItem: React.FC<{
onDelete: () => void;
}> = ({ message, isFocus, onDelete, updateMessage, onSubmit, loading }) => {
const intl = useIntl();
const [isTyping, setIsTyping] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);
const [currentIsFocus, setCurrentIsFocus] = useState(isFocus);
const [imgList, setImgList] = useState<{ uid: number; dataUrl: string }[]>(
[]
);
const [imgList, setImgList] = useState<
{ uid: number | string; dataUrl: string }[]
>([]);
const imgCountRef = useRef(0);
const inputRef = useRef<any>(null);
@ -41,26 +41,6 @@ const MessageItem: React.FC<{
}
}, [isFocus]);
// useEffect(() => {
// if (isTyping) return;
// let index = 0;
// const text = message.content;
// if (!text.length) {
// return;
// }
// setMessageContent('');
// setIsAnimating(true);
// const intervalId = setInterval(() => {
// setMessageContent((prev) => prev + text[index]);
// index += 1;
// if (index === text.length) {
// setIsAnimating(false);
// clearInterval(intervalId);
// }
// }, 20);
// return () => clearInterval(intervalId);
// }, [message.content, isTyping]);
const handleUpdateMessage = (params: { role: string; message: string }) => {
updateMessage({
role: params.role,
@ -118,7 +98,7 @@ const MessageItem: React.FC<{
}, []);
const handleDeleteImg = useCallback(
(uid: number) => {
(uid: number | string) => {
const list = imgList.filter((item) => item.uid !== uid);
setImgList(list);
},
@ -126,12 +106,12 @@ const MessageItem: React.FC<{
);
const handleMessageChange = (e: any) => {
// setIsTyping(true);
console.log('e.target.value:', e.target.value);
handleUpdateMessage({ role: message.role, message: e.target.value });
};
const handleBlur = () => {
// setIsTyping(true);
setCurrentIsFocus(false);
};
@ -139,6 +119,14 @@ const MessageItem: React.FC<{
setCurrentIsFocus(true);
};
const handleClickWrapper = (e: any) => {
console.log('e===========', e);
e.stopPropagation();
e.preventDefault();
inputRef.current.focus();
setCurrentIsFocus(true);
};
const handleRoleChange = () => {
const newRoleType =
message.role === Roles.User ? Roles.Assistant : Roles.User;
@ -150,15 +138,54 @@ const MessageItem: React.FC<{
onDelete();
};
const handleOnPaste = (e: any) => {
e.preventDefault();
const text = e.clipboardData.getData('text');
if (text) {
handleUpdateMessage({ role: message.role, message: text });
} else {
getPasteContent(e);
const handleOnPaste = useCallback(
(e: any) => {
const text = e.clipboardData.getData('text');
if (text) {
handleUpdateMessage({
role: message.role,
message: inputRef.current?.resizableTextArea?.textArea?.value || ''
});
} else {
getPasteContent(e);
}
},
[getPasteContent, message.role, message.content, handleUpdateMessage]
);
const handleUpdateImgList = useCallback(
(list: { uid: number | string; dataUrl: string }[]) => {
setImgList((preList) => {
return [...preList, ...list];
});
},
[imgList]
);
const handleDeleteLastImage = useCallback(() => {
if (imgList.length > 0) {
const newImgList = [...imgList];
const lastImage = newImgList.pop();
if (lastImage) {
handleDeleteImg(lastImage.uid);
}
}
};
}, [imgList, handleDeleteImg]);
const handleKeyDown = useCallback(
(event: any) => {
if (
event.key === 'Backspace' &&
message.content === '' &&
imgList.length > 0
) {
// inputref blur
event.preventDefault();
handleDeleteLastImage();
}
},
[message.content, imgList, handleDeleteLastImage]
);
useHotkeys(
HotKeys.SUBMIT,
@ -180,36 +207,45 @@ const MessageItem: React.FC<{
{intl.formatMessage({ id: `playground.${message.role}` })}
</Button>
</div>
<div className="message-content-input">
<div
className={classNames('message-content-input', {
'has-img': imgList.length
})}
onClick={handleClickWrapper}
>
<ThumbImg dataList={imgList} onDelete={handleDeleteImg}></ThumbImg>
<Input.TextArea
ref={inputRef}
style={{ paddingBlock: '12px' }}
style={{ paddingBlock: '12px', paddingTop: 20 }}
value={message.content}
autoSize={true}
variant="filled"
readOnly={loading}
onKeyDown={handleKeyDown}
onChange={handleMessageChange}
onFocus={handleFocus}
onBlur={handleBlur}
onPaste={handleOnPaste}
></Input.TextArea>
</div>
<div className="delete-btn">
<Space size={5}>
<UploadImg handleUpdateImgList={handleUpdateImgList}></UploadImg>
{message.content && (
<CopyButton
text={message.content}
size="small"
shape="default"
type="default"
type="text"
fontSize="12px"
></CopyButton>
)}
<Tooltip title={intl.formatMessage({ id: 'common.button.delete' })}>
<Button
size="small"
type="text"
onClick={handleDelete}
icon={<MinusCircleOutlined />}
icon={<CloseOutlined />}
></Button>
</Tooltip>
</Space>

@ -1,34 +1,41 @@
import { CloseCircleOutlined } from '@ant-design/icons';
import { Space } from 'antd';
import { Image } from 'antd';
import _ from 'lodash';
import React from 'react';
import React, { useCallback } from 'react';
import '../style/thumb-img.less';
const ThumbImg: React.FC<{
dataList: any[];
onDelete: (uid: number) => void;
}> = ({ dataList, onDelete }) => {
const handleOnDelete = (uid: number) => {
onDelete(uid);
};
const handleOnDelete = useCallback(
(uid: number) => {
onDelete(uid);
},
[onDelete]
);
if (_.isEmpty(dataList)) {
return null;
}
return (
<Space wrap size={10} className="thumb-list-wrap">
<div className="thumb-list-wrap">
{_.map(dataList, (item: any) => {
return (
<span key={item.uid} className="thumb-img">
<span
style={{ backgroundImage: `url(${item.dataUrl})` }}
className="img"
></span>
<span className="img">
<Image src={item.dataUrl} width={56} height={56} />
</span>
<span className="del" onClick={() => handleOnDelete(item.uid)}>
<CloseCircleOutlined />
</span>
</span>
);
})}
</Space>
</div>
);
};
export default ThumbImg;
export default React.memo(ThumbImg);

@ -0,0 +1,85 @@
import { FileImageOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Tooltip, Upload } from 'antd';
import type { UploadFile } from 'antd/es/upload';
import { RcFile } from 'antd/es/upload';
import { debounce } from 'lodash';
import React, { useCallback, useRef } from 'react';
interface UploadImgProps {
handleUpdateImgList: (
imgList: { dataUrl: string; uid: number | string }[]
) => void;
}
const UploadImg: React.FC<UploadImgProps> = ({ handleUpdateImgList }) => {
const intl = useIntl();
const uploadRef = useRef<any>(null);
const getBase64 = (file: RcFile): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
};
const debouncedUpdate = useCallback(
debounce((base64List: { dataUrl: string; uid: number | string }[]) => {
handleUpdateImgList(base64List);
}, 300),
[handleUpdateImgList, intl]
);
const handleChange = async (info: any) => {
const { fileList } = info;
const newFileList = await Promise.all(
fileList.map(async (item: UploadFile) => {
if (item.originFileObj && !item.url) {
const base64 = await getBase64(item.originFileObj as RcFile);
item.url = base64;
}
return item;
})
);
if (newFileList.length > 0) {
const base64List = newFileList
.filter((sitem) => sitem.url)
.map((item: UploadFile) => {
return {
dataUrl: item.url as string,
uid: item.uid
};
});
debouncedUpdate(base64List);
}
};
return (
<>
<Upload
ref={uploadRef}
accept="image/*"
multiple
action="/"
fileList={[]}
beforeUpload={(file) => false}
onChange={handleChange}
>
<Tooltip title={intl.formatMessage({ id: 'playground.img.upload' })}>
<Button
size="small"
type="text"
icon={<FileImageOutlined />}
></Button>
</Tooltip>
</Upload>
</>
);
};
export default React.memo(UploadImg);

@ -20,15 +20,28 @@
text-align: left;
width: 100px;
background-color: var(--ant-button-text-hover-bg);
height: 46px;
height: 54px;
}
}
.message-content-input {
flex: 1;
cursor: pointer;
.ant-input {
padding-right: 40px;
&.has-img {
border: 1px solid var(--ant-color-fill-secondary);
border-radius: var(--border-radius-base);
overflow: hidden;
.ant-input {
border-radius: 0 0 var(--border-radius-base) var(--border-radius-base);
border-color: transparent;
background-color: transparent;
}
&:focus-within {
border-color: var(--ant-color-primary);
}
}
}

@ -11,10 +11,10 @@
overflow: hidden;
border-radius: var(--border-radius-base);
cursor: pointer;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
border: 1px solid var(--ant-color-border);
// background-position: center;
// background-size: cover;
// background-repeat: no-repeat;
// border: 1px solid var(--ant-color-border);
}
.del {
@ -37,6 +37,8 @@
}
.thumb-list-wrap {
display: flex;
gap: 10px;
flex-wrap: wrap;
padding: 10px;
// background-color: var(--color-fill-1);
}

Loading…
Cancel
Save