fetch remote icon

pull/22091/head
JzoNg 11 months ago
parent 10e5cb4382
commit 14a1bba9b7

@ -1,6 +1,7 @@
'use client' 'use client'
import React, { useState } from 'react' import React, { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { getDomain } from 'tldts'
import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
import AppIconPicker from '@/app/components/base/app-icon-picker' import AppIconPicker from '@/app/components/base/app-icon-picker'
import type { AppIconSelection } from '@/app/components/base/app-icon-picker' import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
@ -12,6 +13,7 @@ import type { AppIconType } from '@/types/app'
import type { ToolWithProvider } from '@/app/components/workflow/types' import type { ToolWithProvider } from '@/app/components/workflow/types'
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { uploadRemoteFileInfo } from '@/service/common'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
export type DuplicateAppModalProps = { export type DuplicateAppModalProps = {
@ -59,6 +61,7 @@ const MCPModal = ({
const [appIcon, setAppIcon] = useState<AppIconSelection>(getIcon(data)) const [appIcon, setAppIcon] = useState<AppIconSelection>(getIcon(data))
const [showAppIconPicker, setShowAppIconPicker] = useState(false) const [showAppIconPicker, setShowAppIconPicker] = useState(false)
const [serverIdentifier, setServerIdentifier] = React.useState(data?.server_identifier || '') const [serverIdentifier, setServerIdentifier] = React.useState(data?.server_identifier || '')
const [isFetchingIcon, setIsFetchingIcon] = useState(false)
const isValidUrl = (string: string) => { const isValidUrl = (string: string) => {
try { try {
@ -70,12 +73,36 @@ const MCPModal = ({
} }
} }
const handleBlur = async (url: string) => {
if (data)
return
if (!isValidUrl(url))
return
const domain = getDomain(url)
const remoteIcon = `https://www.google.com/s2/favicons?domain=${domain}&sz=128`
setIsFetchingIcon(true)
try {
const res = await uploadRemoteFileInfo(remoteIcon)
setAppIcon({ type: 'image', url: res.url, fileId: extractFileId(res.url) || '' })
}
catch (e) {
console.error('Failed to fetch remote icon:', e)
Toast.notify({ type: 'error', message: 'Failed to fetch remote icon' })
}
finally {
setIsFetchingIcon(false)
}
}
const submit = async () => { const submit = async () => {
if (!isValidUrl(url)) { if (!isValidUrl(url)) {
Toast.notify({ type: 'error', message: 'invalid server url' }) Toast.notify({ type: 'error', message: 'invalid server url' })
return return
} }
// TODO server identifier validation if (!serverIdentifier.trim()) {
Toast.notify({ type: 'error', message: 'invalid server identifier' })
return
}
await onConfirm({ await onConfirm({
server_url: originalServerUrl === url ? '[__HIDDEN__]' : url.trim(), server_url: originalServerUrl === url ? '[__HIDDEN__]' : url.trim(),
name, name,
@ -106,6 +133,7 @@ const MCPModal = ({
<Input <Input
value={url} value={url}
onChange={e => setUrl(e.target.value)} onChange={e => setUrl(e.target.value)}
onBlur={e => handleBlur(e.target.value.trim())}
placeholder={t('tools.mcp.modal.serverUrlPlaceholder')} placeholder={t('tools.mcp.modal.serverUrlPlaceholder')}
/> />
{originalServerUrl && originalServerUrl !== url && ( {originalServerUrl && originalServerUrl !== url && (
@ -149,7 +177,7 @@ const MCPModal = ({
</div> </div>
</div> </div>
<div className='flex flex-row-reverse pt-5'> <div className='flex flex-row-reverse pt-5'>
<Button disabled={!name || !url || !serverIdentifier} className='ml-2' variant='primary' onClick={submit}>{data ? t('tools.mcp.modal.save') : t('tools.mcp.modal.confirm')}</Button> <Button disabled={!name || !url || !serverIdentifier || isFetchingIcon} className='ml-2' variant='primary' onClick={submit}>{data ? t('tools.mcp.modal.save') : t('tools.mcp.modal.confirm')}</Button>
<Button onClick={onHide}>{t('tools.mcp.modal.cancel')}</Button> <Button onClick={onHide}>{t('tools.mcp.modal.cancel')}</Button>
</div> </div>
</Modal> </Modal>

@ -144,6 +144,7 @@
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",
"swr": "^2.3.0", "swr": "^2.3.0",
"tailwind-merge": "^2.5.4", "tailwind-merge": "^2.5.4",
"tldts": "^7.0.9",
"use-context-selector": "^2.0.0", "use-context-selector": "^2.0.0",
"uuid": "^10.0.0", "uuid": "^10.0.0",
"zod": "^3.23.8", "zod": "^3.23.8",

@ -329,6 +329,9 @@ importers:
tailwind-merge: tailwind-merge:
specifier: ^2.5.4 specifier: ^2.5.4
version: 2.6.0 version: 2.6.0
tldts:
specifier: ^7.0.9
version: 7.0.9
use-context-selector: use-context-selector:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0(react@19.0.0)(scheduler@0.23.2) version: 2.0.0(react@19.0.0)(scheduler@0.23.2)
@ -8039,6 +8042,13 @@ packages:
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}
tldts-core@7.0.9:
resolution: {integrity: sha512-/FGY1+CryHsxF9SFiPZlMOcwQsfABkAvOJO5VEKE8TNifVEqgMF7+UVXHGhm1z4gPUfvVS/EYcwhiRU3vUa1ag==}
tldts@7.0.9:
resolution: {integrity: sha512-/nFtBeNs9nAKIAZE1i3ssOAroci8UqRldFVw5H6RCsNZw7NzDr+Yc3Ek7Tm8XSQKMzw7NSyRSszNxCM0ENsUbg==}
hasBin: true
tmpl@1.0.5: tmpl@1.0.5:
resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==}
@ -18039,6 +18049,12 @@ snapshots:
tinyspy@3.0.2: {} tinyspy@3.0.2: {}
tldts-core@7.0.9: {}
tldts@7.0.9:
dependencies:
tldts-core: 7.0.9
tmpl@1.0.5: {} tmpl@1.0.5: {}
to-regex-range@5.0.1: to-regex-range@5.0.1:

Loading…
Cancel
Save