style: upload image in message input

This commit is contained in:
jialin
2024-09-22 14:55:23 +08:00
parent a5cebb88ef
commit af077c4706
9 changed files with 217 additions and 66 deletions
+2 -2
View File
@@ -2,7 +2,7 @@ import { platformCall } from '@/utils';
const platform = platformCall(); const platform = platformCall();
const KeybindingsMap = { const KeybindingsMap = {
CREATE: ['alt+ctrl+N', 'alt+meta+N'], 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'], RIGHT: ['ctrl+RIGHT', 'meta+RIGHT'],
SAVE: ['ctrl+S', 'meta+S'], SAVE: ['ctrl+S', 'meta+S'],
SUBMIT: ['ctrl+enter', 'meta+enter'], SUBMIT: ['ctrl+enter', 'meta+enter'],
@@ -36,7 +36,7 @@ const KeybiningList: KeybindingValue[] = Object.entries(KeybindingsMap).map(
keybinding: keybinding, keybinding: keybinding,
command: key, command: key,
textKeybinding: platform.isMac textKeybinding: platform.isMac
? keybinding.replace('meta', 'Command').replace('alt', 'Option') ? keybinding.replace('meta', 'Cmd').replace('alt', 'Option')
: keybinding.replace('ctrl', 'Ctrl'), : keybinding.replace('ctrl', 'Ctrl'),
iconKeybinding: platform.isMac iconKeybinding: platform.isMac
? keybinding.replace('meta', '⌘').replace('alt', '⌥') ? keybinding.replace('meta', '⌘').replace('alt', '⌥')
+5 -1
View File
@@ -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.', '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': '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.', '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'
}; };
+5 -1
View File
@@ -29,5 +29,9 @@ export default {
'停止序列是一个预定义或用户指定的文本字符串,当这些序列出现时,它会提示 AI 停止生成后续的 token。', '停止序列是一个预定义或用户指定的文本字符串,当这些序列出现时,它会提示 AI 停止生成后续的 token。',
'playground.viewcode.tips': 'playground.viewcode.tips':
'{here} 查看 API 密钥。您应该使用环境变量或密钥管理工具将您的密钥暴露给您的应用程序。', '{here} 查看 API 密钥。您应该使用环境变量或密钥管理工具将您的密钥暴露给您的应用程序。',
'playground.viewcode.here': '这里' 'playground.viewcode.here': '这里',
'playground.delete.img': '删除图片',
'playground.img.upload': '上传图片',
'playground.img.upload.success': '上传成功',
'playground.img.upload.error': '上传失败'
}; };
+1 -1
View File
@@ -152,7 +152,7 @@ export async function queryHuggingfaceModels(
...params, ...params,
...options, ...options,
limit: 100, limit: 100,
additionalFields: ['sha'], additionalFields: ['sha', 'tags'],
fetch(url: string, config: any) { fetch(url: string, config: any) {
try { try {
const newUrl = params.search.sort const newUrl = params.search.sort
@@ -1,14 +1,16 @@
import CopyButton from '@/components/copy-button'; import CopyButton from '@/components/copy-button';
import HotKeys from '@/config/hotkeys'; import HotKeys from '@/config/hotkeys';
import { MinusCircleOutlined } from '@ant-design/icons'; import { CloseOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max'; import { useIntl } from '@umijs/max';
import { Button, Input, Space, Tooltip } from 'antd'; import { Button, Input, Space, Tooltip } from 'antd';
import classNames from 'classnames';
import _ from 'lodash'; import _ from 'lodash';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { Roles } from '../config'; import { Roles } from '../config';
import '../style/message-item.less'; import '../style/message-item.less';
import ThumbImg from './thumb-img'; import ThumbImg from './thumb-img';
import UploadImg from './upload-img';
interface MessageItemProps { interface MessageItemProps {
role: string; role: string;
content: string; content: string;
@@ -25,12 +27,10 @@ const MessageItem: React.FC<{
onDelete: () => void; onDelete: () => void;
}> = ({ message, isFocus, onDelete, updateMessage, onSubmit, loading }) => { }> = ({ message, isFocus, onDelete, updateMessage, onSubmit, loading }) => {
const intl = useIntl(); const intl = useIntl();
const [isTyping, setIsTyping] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);
const [currentIsFocus, setCurrentIsFocus] = useState(isFocus); 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 imgCountRef = useRef(0);
const inputRef = useRef<any>(null); const inputRef = useRef<any>(null);
@@ -41,26 +41,6 @@ const MessageItem: React.FC<{
} }
}, [isFocus]); }, [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 }) => { const handleUpdateMessage = (params: { role: string; message: string }) => {
updateMessage({ updateMessage({
role: params.role, role: params.role,
@@ -118,7 +98,7 @@ const MessageItem: React.FC<{
}, []); }, []);
const handleDeleteImg = useCallback( const handleDeleteImg = useCallback(
(uid: number) => { (uid: number | string) => {
const list = imgList.filter((item) => item.uid !== uid); const list = imgList.filter((item) => item.uid !== uid);
setImgList(list); setImgList(list);
}, },
@@ -126,12 +106,12 @@ const MessageItem: React.FC<{
); );
const handleMessageChange = (e: any) => { const handleMessageChange = (e: any) => {
// setIsTyping(true); console.log('e.target.value:', e.target.value);
handleUpdateMessage({ role: message.role, message: e.target.value }); handleUpdateMessage({ role: message.role, message: e.target.value });
}; };
const handleBlur = () => { const handleBlur = () => {
// setIsTyping(true);
setCurrentIsFocus(false); setCurrentIsFocus(false);
}; };
@@ -139,6 +119,14 @@ const MessageItem: React.FC<{
setCurrentIsFocus(true); setCurrentIsFocus(true);
}; };
const handleClickWrapper = (e: any) => {
console.log('e===========', e);
e.stopPropagation();
e.preventDefault();
inputRef.current.focus();
setCurrentIsFocus(true);
};
const handleRoleChange = () => { const handleRoleChange = () => {
const newRoleType = const newRoleType =
message.role === Roles.User ? Roles.Assistant : Roles.User; message.role === Roles.User ? Roles.Assistant : Roles.User;
@@ -150,15 +138,54 @@ const MessageItem: React.FC<{
onDelete(); onDelete();
}; };
const handleOnPaste = (e: any) => { const handleOnPaste = useCallback(
e.preventDefault(); (e: any) => {
const text = e.clipboardData.getData('text'); const text = e.clipboardData.getData('text');
if (text) { if (text) {
handleUpdateMessage({ role: message.role, message: text }); handleUpdateMessage({
} else { role: message.role,
getPasteContent(e); 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( useHotkeys(
HotKeys.SUBMIT, HotKeys.SUBMIT,
@@ -180,36 +207,45 @@ const MessageItem: React.FC<{
{intl.formatMessage({ id: `playground.${message.role}` })} {intl.formatMessage({ id: `playground.${message.role}` })}
</Button> </Button>
</div> </div>
<div className="message-content-input"> <div
className={classNames('message-content-input', {
'has-img': imgList.length
})}
onClick={handleClickWrapper}
>
<ThumbImg dataList={imgList} onDelete={handleDeleteImg}></ThumbImg> <ThumbImg dataList={imgList} onDelete={handleDeleteImg}></ThumbImg>
<Input.TextArea <Input.TextArea
ref={inputRef} ref={inputRef}
style={{ paddingBlock: '12px' }} style={{ paddingBlock: '12px', paddingTop: 20 }}
value={message.content} value={message.content}
autoSize={true} autoSize={true}
variant="filled" variant="filled"
readOnly={loading} readOnly={loading}
onKeyDown={handleKeyDown}
onChange={handleMessageChange} onChange={handleMessageChange}
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
onPaste={handleOnPaste}
></Input.TextArea> ></Input.TextArea>
</div> </div>
<div className="delete-btn"> <div className="delete-btn">
<Space size={5}> <Space size={5}>
<UploadImg handleUpdateImgList={handleUpdateImgList}></UploadImg>
{message.content && ( {message.content && (
<CopyButton <CopyButton
text={message.content} text={message.content}
size="small" size="small"
shape="default" shape="default"
type="default" type="text"
fontSize="12px" fontSize="12px"
></CopyButton> ></CopyButton>
)} )}
<Tooltip title={intl.formatMessage({ id: 'common.button.delete' })}> <Tooltip title={intl.formatMessage({ id: 'common.button.delete' })}>
<Button <Button
size="small" size="small"
type="text"
onClick={handleDelete} onClick={handleDelete}
icon={<MinusCircleOutlined />} icon={<CloseOutlined />}
></Button> ></Button>
</Tooltip> </Tooltip>
</Space> </Space>
+19 -12
View File
@@ -1,34 +1,41 @@
import { CloseCircleOutlined } from '@ant-design/icons'; import { CloseCircleOutlined } from '@ant-design/icons';
import { Space } from 'antd'; import { Image } from 'antd';
import _ from 'lodash'; import _ from 'lodash';
import React from 'react'; import React, { useCallback } from 'react';
import '../style/thumb-img.less'; import '../style/thumb-img.less';
const ThumbImg: React.FC<{ const ThumbImg: React.FC<{
dataList: any[]; dataList: any[];
onDelete: (uid: number) => void; onDelete: (uid: number) => void;
}> = ({ dataList, onDelete }) => { }> = ({ dataList, onDelete }) => {
const handleOnDelete = (uid: number) => { const handleOnDelete = useCallback(
onDelete(uid); (uid: number) => {
}; onDelete(uid);
},
[onDelete]
);
if (_.isEmpty(dataList)) {
return null;
}
return ( return (
<Space wrap size={10} className="thumb-list-wrap"> <div className="thumb-list-wrap">
{_.map(dataList, (item: any) => { {_.map(dataList, (item: any) => {
return ( return (
<span key={item.uid} className="thumb-img"> <span key={item.uid} className="thumb-img">
<span <span className="img">
style={{ backgroundImage: `url(${item.dataUrl})` }} <Image src={item.dataUrl} width={56} height={56} />
className="img" </span>
></span>
<span className="del" onClick={() => handleOnDelete(item.uid)}> <span className="del" onClick={() => handleOnDelete(item.uid)}>
<CloseCircleOutlined /> <CloseCircleOutlined />
</span> </span>
</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);
+16 -3
View File
@@ -20,15 +20,28 @@
text-align: left; text-align: left;
width: 100px; width: 100px;
background-color: var(--ant-button-text-hover-bg); background-color: var(--ant-button-text-hover-bg);
height: 46px; height: 54px;
} }
} }
.message-content-input { .message-content-input {
flex: 1; flex: 1;
cursor: pointer;
.ant-input { &.has-img {
padding-right: 40px; 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);
}
} }
} }
+7 -5
View File
@@ -11,10 +11,10 @@
overflow: hidden; overflow: hidden;
border-radius: var(--border-radius-base); border-radius: var(--border-radius-base);
cursor: pointer; cursor: pointer;
background-position: center; // background-position: center;
background-size: cover; // background-size: cover;
background-repeat: no-repeat; // background-repeat: no-repeat;
border: 1px solid var(--ant-color-border); // border: 1px solid var(--ant-color-border);
} }
.del { .del {
@@ -37,6 +37,8 @@
} }
.thumb-list-wrap { .thumb-list-wrap {
display: flex;
gap: 10px;
flex-wrap: wrap;
padding: 10px; padding: 10px;
// background-color: var(--color-fill-1);
} }