From af077c47068dc4da414cbfb2d78e22b871174cb8 Mon Sep 17 00:00:00 2001 From: jialin Date: Wed, 4 Sep 2024 11:05:12 +0800 Subject: [PATCH] style: upload image in message input --- src/config/hotkeys.ts | 4 +- src/locales/en-US/playground.ts | 6 +- src/locales/zh-CN/playground.ts | 6 +- src/pages/llmodels/apis/index.ts | 2 +- .../playground/components/message-item.tsx | 118 ++++++++++++------ src/pages/playground/components/thumb-img.tsx | 31 +++-- .../playground/components/upload-img.tsx | 85 +++++++++++++ src/pages/playground/style/message-item.less | 19 ++- src/pages/playground/style/thumb-img.less | 12 +- 9 files changed, 217 insertions(+), 66 deletions(-) create mode 100644 src/pages/playground/components/upload-img.tsx diff --git a/src/config/hotkeys.ts b/src/config/hotkeys.ts index 09347d81..607b993f 100644 --- a/src/config/hotkeys.ts +++ b/src/config/hotkeys.ts @@ -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', '⌥') diff --git a/src/locales/en-US/playground.ts b/src/locales/en-US/playground.ts index 4be2b1a6..3794c6ec 100644 --- a/src/locales/en-US/playground.ts +++ b/src/locales/en-US/playground.ts @@ -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' }; diff --git a/src/locales/zh-CN/playground.ts b/src/locales/zh-CN/playground.ts index d727216a..fd86491d 100644 --- a/src/locales/zh-CN/playground.ts +++ b/src/locales/zh-CN/playground.ts @@ -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': '上传失败' }; diff --git a/src/pages/llmodels/apis/index.ts b/src/pages/llmodels/apis/index.ts index e498dab2..80ee45ac 100644 --- a/src/pages/llmodels/apis/index.ts +++ b/src/pages/llmodels/apis/index.ts @@ -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 diff --git a/src/pages/playground/components/message-item.tsx b/src/pages/playground/components/message-item.tsx index 139f2ae9..bc8f9582 100644 --- a/src/pages/playground/components/message-item.tsx +++ b/src/pages/playground/components/message-item.tsx @@ -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(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}` })} -
+
+ {message.content && ( )} diff --git a/src/pages/playground/components/thumb-img.tsx b/src/pages/playground/components/thumb-img.tsx index f4b75327..faa64740 100644 --- a/src/pages/playground/components/thumb-img.tsx +++ b/src/pages/playground/components/thumb-img.tsx @@ -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 ( - +
{_.map(dataList, (item: any) => { return ( - + + + + handleOnDelete(item.uid)}> ); })} - +
); }; -export default ThumbImg; +export default React.memo(ThumbImg); diff --git a/src/pages/playground/components/upload-img.tsx b/src/pages/playground/components/upload-img.tsx new file mode 100644 index 00000000..49ebfea7 --- /dev/null +++ b/src/pages/playground/components/upload-img.tsx @@ -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 = ({ handleUpdateImgList }) => { + const intl = useIntl(); + const uploadRef = useRef(null); + + const getBase64 = (file: RcFile): Promise => { + 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 ( + <> + false} + onChange={handleChange} + > + + + + + + ); +}; + +export default React.memo(UploadImg); diff --git a/src/pages/playground/style/message-item.less b/src/pages/playground/style/message-item.less index d6c31ac1..b70137fe 100644 --- a/src/pages/playground/style/message-item.less +++ b/src/pages/playground/style/message-item.less @@ -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); + } } } diff --git a/src/pages/playground/style/thumb-img.less b/src/pages/playground/style/thumb-img.less index 4951155e..e2248495 100644 --- a/src/pages/playground/style/thumb-img.less +++ b/src/pages/playground/style/thumb-img.less @@ -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); }