mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
115 lines
4.2 KiB
TypeScript
115 lines
4.2 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
import { cn } from '@langgenius/dify-ui/cn'
|
|
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
|
import { memo, useMemo, useRef, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import AppIcon from '@/app/components/base/app-icon'
|
|
import { Group } from '@/app/components/base/icons/src/vender/other'
|
|
import {
|
|
useAllBuiltInTools,
|
|
useAllCustomTools,
|
|
useAllMCPTools,
|
|
useAllWorkflowTools,
|
|
} from '@/service/use-tools'
|
|
import { getIconFromMarketPlace } from '@/utils/get-icon'
|
|
|
|
type Status = 'not-installed' | 'not-authorized' | undefined
|
|
|
|
export type ToolIconProps = {
|
|
id: string
|
|
providerName: string
|
|
}
|
|
|
|
export const ToolIcon = memo(({ providerName }: ToolIconProps) => {
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
const { data: buildInTools } = useAllBuiltInTools()
|
|
const { data: customTools } = useAllCustomTools()
|
|
const { data: workflowTools } = useAllWorkflowTools()
|
|
const { data: mcpTools } = useAllMCPTools()
|
|
const isDataReady = !!buildInTools && !!customTools && !!workflowTools && !!mcpTools
|
|
const currentProvider = useMemo(() => {
|
|
const mergedTools = [
|
|
...(buildInTools || []),
|
|
...(customTools || []),
|
|
...(workflowTools || []),
|
|
...(mcpTools || []),
|
|
]
|
|
return mergedTools.find((toolWithProvider) => {
|
|
return toolWithProvider.name === providerName || toolWithProvider.id === providerName
|
|
})
|
|
}, [buildInTools, customTools, providerName, workflowTools, mcpTools])
|
|
|
|
const providerNameParts = providerName.split('/')
|
|
const author = providerNameParts[0]
|
|
const name = providerNameParts[1]
|
|
const icon = useMemo(() => {
|
|
if (!isDataReady) return ''
|
|
if (currentProvider) return currentProvider.icon
|
|
const iconFromMarketPlace = getIconFromMarketPlace(`${author}/${name}`)
|
|
return iconFromMarketPlace
|
|
}, [author, currentProvider, name, isDataReady])
|
|
const status: Status = useMemo(() => {
|
|
if (!isDataReady) return undefined
|
|
if (!currentProvider) return 'not-installed'
|
|
if (currentProvider.is_team_authorization === false) return 'not-authorized'
|
|
return undefined
|
|
}, [currentProvider, isDataReady])
|
|
const indicator =
|
|
status === 'not-installed' ? 'error' : status === 'not-authorized' ? 'warning' : undefined
|
|
const notSuccess = (['not-installed', 'not-authorized'] as Array<Status>).includes(status)
|
|
const { t } = useTranslation()
|
|
const tooltip = useMemo(() => {
|
|
if (!notSuccess) return undefined
|
|
if (status === 'not-installed')
|
|
return t(($) => $['nodes.agent.toolNotInstallTooltip'], { ns: 'workflow', tool: name })
|
|
if (status === 'not-authorized')
|
|
return t(($) => $['nodes.agent.toolNotAuthorizedTooltip'], { ns: 'workflow', tool: name })
|
|
throw new Error('Unknown status')
|
|
}, [name, notSuccess, status, t])
|
|
const [iconFetchError, setIconFetchError] = useState(false)
|
|
let iconContent: ReactNode = <Group className="size-3 opacity-35" />
|
|
|
|
if (!iconFetchError && icon) {
|
|
if (typeof icon === 'string') {
|
|
iconContent = (
|
|
<img
|
|
src={icon}
|
|
alt="tool icon"
|
|
className={cn('size-3.5 size-full object-cover', notSuccess && 'opacity-50')}
|
|
onError={() => setIconFetchError(true)}
|
|
/>
|
|
)
|
|
} else if (typeof icon === 'object') {
|
|
iconContent = (
|
|
<AppIcon
|
|
className={cn('size-3.5 size-full object-cover', notSuccess && 'opacity-50')}
|
|
icon={icon?.content}
|
|
background={icon?.background}
|
|
/>
|
|
)
|
|
}
|
|
}
|
|
|
|
const iconNode = (
|
|
<div className={cn('relative')} ref={containerRef}>
|
|
<div className="flex size-5 items-center justify-center overflow-hidden rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge">
|
|
{iconContent}
|
|
</div>
|
|
{indicator && <StatusDot status={indicator} className="absolute -top-px -right-px" />}
|
|
{tooltip && <span className="sr-only">{tooltip}</span>}
|
|
</div>
|
|
)
|
|
|
|
if (!notSuccess || !tooltip) return iconNode
|
|
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger render={iconNode} />
|
|
<TooltipContent>{tooltip}</TooltipContent>
|
|
</Tooltip>
|
|
)
|
|
})
|
|
|
|
ToolIcon.displayName = 'ToolIcon'
|