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
+2 -1
View File
@@ -81,6 +81,7 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'clusters.addworker.extraVolume.holder':
'e.g. /data/models (path must start with /)',
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
'clusters.button.genToken': 'Generate Token',
'clusters.button.genToken':
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`
};
+6 -1
View File
@@ -156,5 +156,10 @@ export default {
'playground.image.generate.error':
'Something went wrong. The image could not be generated.',
'playground.uploadfile.sizeError':
'File size exceeds the limit. Maximum allowed: {size}.'
'File size exceeds the limit. Maximum allowed: {size}.',
'playground.uploadImage.url.invalid':
'Please enter a direct .png or .jpg image URL (e.g. https://.../image.png). Press ESC to cancel.',
'playground.uploadImage.url.holder':
'Enter an image URL (supports .png, .jpeg, .jpg)',
'playground.uploadImage.url.button': 'Add Image from URL'
};
+3 -2
View File
@@ -81,7 +81,8 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
'clusters.addworker.extraVolume.holder':
'e.g. /data/models (path must start with /)',
'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
'clusters.button.genToken': 'Generate Token',
'clusters.button.genToken':
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`
};
@@ -154,6 +155,6 @@ Same applies to the <span class="bold-text">/opt/dtk</span> directory.`,
// 66. 'clusters.addworker.autoDetect': 'Auto-detect',
// 67. 'clusters.addworker.extraVolume.holder': 'e.g. /data/models (path must start with /)'
// 68. 'clusters.addworker.vendorNotes.title': 'Notes for {vendor} Device',
// 69. 'clusters.button.genToken': 'Generate Token',
// 69. 'clusters.button.genToken': 'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
// 70. 'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`
// ========== End of To-Do List ==========
+9 -1
View File
@@ -159,11 +159,19 @@ export default {
'playground.image.generate.error':
'Something went wrong. The image could not be generated.',
'playground.uploadfile.sizeError':
'File size exceeds the limit. Maximum allowed: {size}.'
'File size exceeds the limit. Maximum allowed: {size}.',
'playground.uploadImage.url.invalid':
'Please enter a direct .png or .jpg image URL (e.g. https://.../image.png). Press ESC to cancel.',
'playground.uploadImage.url.holder':
'Enter an image URL (supports .png, .jpeg, .jpg)',
'playground.uploadImage.url.button': 'Add Image from URL'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'playground.rerank.query.validate': 'The query is required.'
// 2. 'playground.image.generate.error': 'Something went wrong. The image could not be generated.',
// 3. 'playground.uploadfile.sizeError': 'File size exceeds the limit. Maximum allowed: {size}.'
// 4. 'playground.uploadImage.url.invalid': 'Please enter a direct .png or .jpg image URL(e.g. https://.../image.png). Press ESC to cancel.',
// 5. 'playground.uploadImage.url.holder': 'Enter an image URL (supports .png, .jpeg, .jpg)'
// 6. 'playground.uploadImage.url.button': 'Add Image from URL'
// ========== End of To-Do List ==========
+4 -2
View File
@@ -81,10 +81,12 @@ export default {
'clusters.addworker.extraVolume.holder':
'напр. /data/models (путь должен начинаться с /)',
'clusters.addworker.vendorNotes.title': 'Примечания для устройств {vendor}',
'clusters.button.genToken': 'Сгенерировать токен',
'clusters.button.genToken':
'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`
// 1. 'clusters.addworker.amdNotes-01': `If the <span class="bold-text">/opt/rocm</span> directory does not exist, please create a symbolic link pointing to the ROCm installed path: <span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>.`,
// 2. 'clusters.button.genToken': 'Need to create a new token? Click <a href="{link}" target="_blank">here</a>.',
// ================================================================
+12 -3
View File
@@ -150,10 +150,19 @@ export default {
'playground.model.noavailable.tips2':
'Если нужная модель не отображается, убедитесь, что она запущена и ей присвоена правильная категория. Если категория указана неверно, её можно изменить вручную в настройках модели.',
'playground.rerank.query.validate': 'Необходимо указать запрос.',
'playground.image.generate.error': 'Произошла ошибка. Не удалось сгенерировать изображение.',
'playground.uploadfile.sizeError': 'Размер файла превышает ограничение. Максимальный размер: {size}.'
'playground.image.generate.error':
'Произошла ошибка. Не удалось сгенерировать изображение.',
'playground.uploadfile.sizeError':
'Размер файла превышает ограничение. Максимальный размер: {size}.',
'playground.uploadImage.url.invalid':
'Please enter a direct .png or .jpg image URL (e.g. https://.../image.png). Press ESC to cancel.',
'playground.uploadImage.url.holder':
'Enter an image URL (supports .png, .jpeg, .jpg)',
'playground.uploadImage.url.button': 'Add Image from URL'
};
// ========== To-Do: Translate Keys (Remove After Translation) ==========
// 1. 'playground.uploadImage.url.invalid': 'Please enter a direct .png or .jpg image URL(e.g. https://.../image.png). Press ESC to cancel.',
// 2. 'playground.uploadImage.url.holder': 'Enter an image URL (supports .png, .jpeg, .jpg)',
// 3. 'playground.uploadImage.url.button': 'Add Image from URL'
// ========== End of To-Do List ==========
+2 -1
View File
@@ -79,7 +79,8 @@ export default {
'clusters.addworker.extraVolume.holder':
'例如:/data/models(路径需以 / 开头)',
'clusters.addworker.vendorNotes.title': '{vendor}设备注意事项',
'clusters.button.genToken': '生成令牌',
'clusters.button.genToken':
'需要创建令牌?点击<a href="{link}" target="_blank">这里</a>。',
'clusters.addworker.amdNotes-01':
'如果 <span class="bold-text">/opt/rocm</span> 目录不存在,请创建一个指向已安装 ROCm 路径的符号链接:<span class="bold-text">ln -s /path/to/rocm /opt/rocm</span>。'
};
+7 -1
View File
@@ -148,5 +148,11 @@ export default {
'若预期的模型未显示,请检查模型是否已正常运行并被正确分类。如分类不正确,请编辑模型并手动调整其类别。',
'playground.rerank.query.validate': '查询内容不能为空',
'playground.image.generate.error': '出了一点问题,图片未能生成。',
'playground.uploadfile.sizeError': '上传的文件大小超过限制,最大允许 {size}。'
'playground.uploadfile.sizeError':
'上传的文件大小超过限制,最大允许 {size}。',
'playground.uploadImage.url.invalid':
'请输入直接的 .png 或 .jpg 图片链接(例如:https://…/image.png)。按 ESC 可取消。',
'playground.uploadImage.url.holder':
'请输入图片链接(支持 .png、.jpeg、.jpg 格式)',
'playground.uploadImage.url.button': '从链接添加图片'
};
+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
};
};