feat: add image from url
This commit is contained in:
@@ -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>.`
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
@@ -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 ==========
|
||||
|
||||
@@ -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 ==========
|
||||
|
||||
@@ -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>.',
|
||||
// ================================================================
|
||||
|
||||
@@ -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 ==========
|
||||
|
||||
@@ -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>。'
|
||||
};
|
||||
|
||||
@@ -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': '从链接添加图片'
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user