feat: add image from url

This commit is contained in:
jialin
2025-12-01 19:01:33 +08:00
parent 14f0afec08
commit 5cbb38922c
13 changed files with 187 additions and 31 deletions
+1 -1
View File
@@ -198,7 +198,7 @@ export const yamlTemplate = `# ----------------------------------------
# - must be endwith '-custom'
# version_configs:
# - image_name: required
# - run_command: required
# - run_command: optional
# - custom_framework:
# - optional
# - choose from: ${Object.values(GPUDriverMap).join(', ')}, CPU
@@ -4,9 +4,8 @@ import SealInput from '@/components/seal-form/seal-input';
import { PageAction } from '@/config';
import { PageActionType } from '@/config/types';
import useAppUtils from '@/hooks/use-app-utils';
import { ExportOutlined } from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Form, Typography } from 'antd';
import { Form } from 'antd';
import React, { useEffect } from 'react';
import styled from 'styled-components';
import { ProviderType, ProviderValueMap } from '../config';
@@ -117,17 +116,21 @@ const AddModal: React.FC<AddModalProps> = ({
id: 'clusters.credential.token'
})}
required={action === PageAction.CREATE}
description={
<span
dangerouslySetInnerHTML={{
__html: intl.formatMessage(
{
id: 'clusters.button.genToken'
},
{
link: 'https://cloud.digitalocean.com/account/api/tokens'
}
)
}}
></span>
}
></SealInput.Password>
<ExtraContent>
<Typography.Link
href="https://cloud.digitalocean.com/account/api/tokens"
target="_blank"
rel="noopener noreferrer"
>
{intl.formatMessage({ id: 'clusters.button.genToken' })}{' '}
<ExportOutlined style={{ transform: 'scale(0.8)' }} />
</Typography.Link>
</ExtraContent>
</Form.Item>
</>
)}
@@ -1,4 +1,5 @@
import SimpleAudio from '@/components/audio-player/simple-audio';
import DropDownActions from '@/components/drop-down-actions';
import IconFont from '@/components/icon-font';
import UploadAudio from '@/components/upload-audio';
import HotKeys, { KeyMap } from '@/config/hotkeys';
@@ -10,8 +11,11 @@ import {
import {
ClearOutlined,
CustomerServiceOutlined,
LinkOutlined,
PictureOutlined,
SendOutlined,
SwapOutlined
SwapOutlined,
UploadOutlined
} from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Checkbox, Divider, Input, Tooltip } from 'antd';
@@ -163,8 +167,11 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
content: '',
imgs: []
});
const [isFromUrl, setIsFromUrl] = useState(false);
const [openImgTips, setOpenImgTips] = useState(false);
const uidCountRef = useRef(0);
const inputRef = useRef<any>(null);
const inputImgRef = useRef<any>(null);
const updateUidCount = () => {
uidCountRef.current += 1;
@@ -293,6 +300,39 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
});
};
const handleInputImageUrl = (e: any) => {
const url = e.target.value;
// ends with .png, .jpg, .jpeg
const regex = /(https?:\/\/\S+\.(png|jpg|jpeg))/gi;
const isValid = regex.test(url);
setOpenImgTips(!isValid);
if (!isValid) {
return;
}
handleUpdateImgList([
{
uid: updateUidCount(),
dataUrl: e.target.value
}
]);
setIsFromUrl(false);
setOpenImgTips(false);
};
const handleOnEscape = (e: any) => {
if (e.key === 'Escape') {
setIsFromUrl(false);
setOpenImgTips(false);
}
};
const handleAddImgFromUrl = () => {
setIsFromUrl(true);
setTimeout(() => {
inputImgRef.current?.focus?.();
}, 100);
};
const handleUploadAudioChange = async (data: {
file: any;
fileList: any[];
@@ -412,11 +452,41 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
</Checkbox>
)}
{actions.includes('upload') && message.role === Roles.User && (
<UploadImg
handleUpdateImgList={handleUpdateImgList}
size="middle"
></UploadImg>
<DropDownActions
placement={'topLeft'}
menu={{
items: [
{
label: (
<UploadImg
handleUpdateImgList={handleUpdateImgList}
size="middle"
>
{intl.formatMessage({ id: 'playground.img.upload' })}
</UploadImg>
),
key: 'upload_image',
icon: <UploadOutlined />
},
{
label: intl.formatMessage({
id: 'playground.uploadImage.url.button'
}),
key: 'add_image_url',
icon: <LinkOutlined />,
onClick: handleAddImgFromUrl
}
]
}}
>
<Button
type="text"
size="middle"
icon={<PictureOutlined />}
></Button>
</DropDownActions>
)}
{actions.includes('upload') && message.role === Roles.User && (
<UploadAudio
maxFileSize={1024 * 1024}
@@ -533,6 +603,26 @@ const MessageInput: React.FC<MessageInputProps> = forwardRef(
</AudioWrapper>
)}
</ImgsWrapper>
{isFromUrl && (
<Tooltip
open={openImgTips}
title={intl.formatMessage({
id: 'playground.uploadImage.url.invalid'
})}
>
<Input
ref={inputImgRef}
status={openImgTips ? 'error' : ''}
placeholder={intl.formatMessage({
id: 'playground.uploadImage.url.holder'
})}
style={{ width: 360 }}
onBlur={handleInputImageUrl}
onPressEnter={handleInputImageUrl}
onKeyDown={handleOnEscape}
></Input>
</Tooltip>
)}
<div className="input-box">
{actions.includes('paste') ? (
<TextArea
@@ -184,4 +184,4 @@ const UploadImg: React.FC<UploadImgProps> = ({
);
};
export default React.memo(UploadImg);
export default UploadImg;
@@ -0,0 +1,30 @@
import _ from 'lodash';
import { useState } from 'react';
const imageUrlRegex = /(https?:\/\/\S+\.(png|jpg|jpeg))/gi;
const useFindImages = () => {
function extractImageUrls(text: string) {
return [...text.matchAll(imageUrlRegex)].map((m) => m[0]);
}
const [pendingUrls, setPendingUrls] = useState<string[]>([]);
const debounceExtract = _.debounce((text: string) => {
const urls = extractImageUrls(text);
if (urls.length > 0) {
setPendingUrls(urls);
} else {
setPendingUrls([]);
}
}, 200);
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const text = e.target.value;
debounceExtract(text);
};
return {
handleInputChange
};
};