style: upload image in message input
This commit is contained in:
@@ -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', '⌥')
|
||||||
|
|||||||
@@ -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'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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': '上传失败'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user