mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 08:48:10 +08:00
Co-authored-by: zxhlyh <jasonapring2015@outlook.com> Co-authored-by: fatelei <fatelei@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: 姜涵煦 <hanxujiang@jianghanxudeMacBook-Pro-2.local> Co-authored-by: L1nSn0w <l1nsn0w@qq.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
237 lines
8.3 KiB
TypeScript
237 lines
8.3 KiB
TypeScript
'use client'
|
|
import type { Plugin } from '../types'
|
|
import { cn } from '@langgenius/dify-ui/cn'
|
|
import { useAtomValue } from 'jotai'
|
|
import * as React from 'react'
|
|
import { useTranslation } from '#i18n'
|
|
import { useGetLanguage } from '@/context/i18n'
|
|
import { currentWorkspaceIdAtom } from '@/context/workspace-state'
|
|
import useTheme from '@/hooks/use-theme'
|
|
import { renderI18nObject } from '@/i18n-config'
|
|
import { Theme } from '@/types/app'
|
|
import { formatNumber } from '@/utils/format'
|
|
import Partner from '../base/badges/partner'
|
|
import Verified from '../base/badges/verified'
|
|
import Icon from '../card/base/card-icon'
|
|
import { useCategories } from '../hooks'
|
|
import { getPluginCardIconUrl } from '../utils'
|
|
import CornerMark from './base/corner-mark'
|
|
import Description from './base/description'
|
|
import OrgInfo from './base/org-info'
|
|
import Placeholder from './base/placeholder'
|
|
import Title from './base/title'
|
|
|
|
export type CardPayload = Omit<Plugin, 'icon' | 'icon_dark'> & {
|
|
icon: string | { content: string; background: string }
|
|
icon_dark?: string | { content: string; background: string }
|
|
}
|
|
|
|
type Props = Readonly<{
|
|
className?: string
|
|
payload: CardPayload
|
|
titleLeft?: React.ReactNode
|
|
installed?: boolean
|
|
installFailed?: boolean
|
|
hideCornerMark?: boolean
|
|
descriptionLineRows?: number
|
|
footer?: React.ReactNode
|
|
isLoading?: boolean
|
|
loadingFileName?: string
|
|
limitedInstall?: boolean
|
|
compact?: boolean
|
|
variant?: 'default' | 'marketplace'
|
|
}>
|
|
|
|
type CardIconProps = {
|
|
icon: CardPayload['icon']
|
|
installFailed?: boolean
|
|
installed?: boolean
|
|
marketplace?: boolean
|
|
plugin: Pick<Plugin, 'from' | 'name' | 'org' | 'type'>
|
|
}
|
|
|
|
const WorkspaceCardIcon = ({ icon, installFailed, installed, plugin }: CardIconProps) => {
|
|
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom)
|
|
const iconSrc = getPluginCardIconUrl(plugin, icon, currentWorkspaceId)
|
|
|
|
return <Icon src={iconSrc} installed={installed} installFailed={installFailed} />
|
|
}
|
|
|
|
const CardIcon = ({ icon, installFailed, installed, marketplace, plugin }: CardIconProps) => {
|
|
if (marketplace || plugin.from === 'marketplace') {
|
|
const iconSrc = getPluginCardIconUrl({ ...plugin, from: 'marketplace' }, icon, '')
|
|
return <Icon src={iconSrc} installed={installed} installFailed={installFailed} />
|
|
}
|
|
|
|
return (
|
|
<WorkspaceCardIcon
|
|
icon={icon}
|
|
installFailed={installFailed}
|
|
installed={installed}
|
|
plugin={plugin}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const Card = ({
|
|
className,
|
|
payload,
|
|
titleLeft,
|
|
installed,
|
|
installFailed,
|
|
hideCornerMark,
|
|
descriptionLineRows,
|
|
footer,
|
|
isLoading = false,
|
|
loadingFileName,
|
|
limitedInstall = false,
|
|
compact = false,
|
|
variant = 'default',
|
|
}: Props) => {
|
|
const locale = useGetLanguage()
|
|
const { t } = useTranslation()
|
|
const { categoriesMap } = useCategories(true)
|
|
const { category, type, name, org, label, brief, icon, icon_dark, verified, from } = payload
|
|
const badges = payload.badges ?? []
|
|
const { theme } = useTheme()
|
|
const activeIcon = theme === Theme.dark && icon_dark ? icon_dark : icon
|
|
const pluginIdentity = { from, name, org, type }
|
|
const getLocalizedText = (obj: Record<string, string> | undefined) =>
|
|
obj ? renderI18nObject(obj, locale) : ''
|
|
const isPartner = badges.includes('partner')
|
|
const effectiveDescriptionLineRows = descriptionLineRows ?? (compact ? 1 : 2)
|
|
const isMarketplaceVariant = variant === 'marketplace'
|
|
const cornerMarkText = categoriesMap[type === 'bundle' ? type : category]?.label ?? ''
|
|
|
|
const wrapClassName = cn(
|
|
'hover-bg-components-panel-on-panel-item-bg relative overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg shadow-xs',
|
|
isMarketplaceVariant &&
|
|
'h-37 transition-all group-hover:bg-components-panel-on-panel-item-bg-hover group-hover:shadow-md',
|
|
className,
|
|
)
|
|
if (isLoading) {
|
|
return <Placeholder wrapClassName={wrapClassName} loadingFileName={loadingFileName!} />
|
|
}
|
|
|
|
if (isMarketplaceVariant) {
|
|
return (
|
|
<div className={wrapClassName}>
|
|
<div className="relative flex h-full flex-col">
|
|
{!hideCornerMark && <CornerMark text={cornerMarkText} />}
|
|
<div className="flex items-center gap-3 px-4 pt-4 pb-2">
|
|
<CardIcon
|
|
icon={activeIcon}
|
|
installed={installed}
|
|
installFailed={installFailed}
|
|
marketplace
|
|
plugin={pluginIdentity}
|
|
/>
|
|
<div className="flex min-w-0 flex-1 flex-col justify-center gap-0.5">
|
|
<div className="flex h-5 min-w-0 items-center">
|
|
<div className="truncate system-md-medium text-text-primary">
|
|
{getLocalizedText(label)}
|
|
</div>
|
|
{isPartner && (
|
|
<Partner
|
|
className="ml-0.5 size-4"
|
|
text={t(($) => $['marketplace.partnerTip'], { ns: 'plugin' })}
|
|
/>
|
|
)}
|
|
{verified && (
|
|
<Verified
|
|
className="ml-0.5 size-4"
|
|
text={t(($) => $['marketplace.verifiedTip'], { ns: 'plugin' })}
|
|
/>
|
|
)}
|
|
{titleLeft}
|
|
</div>
|
|
<div className="flex h-4 min-w-0 items-center gap-2 system-xs-regular text-text-tertiary">
|
|
{org && (
|
|
<div className="flex min-w-0 items-center gap-1">
|
|
<span className="shrink-0 lowercase">
|
|
{t(($) => $['marketplace.by'], { ns: 'plugin' })}
|
|
</span>
|
|
<span className="truncate">{org}</span>
|
|
</div>
|
|
)}
|
|
{org && payload.install_count !== undefined && (
|
|
<span className="shrink-0 text-text-quaternary">·</span>
|
|
)}
|
|
{payload.install_count !== undefined && (
|
|
<span className="shrink-0">
|
|
{t(($) => $.install, {
|
|
ns: 'plugin',
|
|
num: formatNumber(payload.install_count),
|
|
})}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Description
|
|
className="mx-4 mt-1 text-text-secondary"
|
|
text={getLocalizedText(brief)}
|
|
descriptionLineRows={effectiveDescriptionLineRows}
|
|
/>
|
|
{!!footer && <div className="px-4 pt-2 pr-5">{footer}</div>}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className={wrapClassName}>
|
|
<div className={cn(compact ? 'p-3 pb-2' : 'p-4 pb-3', limitedInstall && 'pb-1')}>
|
|
{!hideCornerMark && <CornerMark text={cornerMarkText} />}
|
|
{/* Header */}
|
|
<div className="flex">
|
|
<CardIcon
|
|
icon={activeIcon}
|
|
installed={installed}
|
|
installFailed={installFailed}
|
|
plugin={pluginIdentity}
|
|
/>
|
|
<div className="ml-3 w-0 grow">
|
|
<div className="flex h-5 items-center">
|
|
<Title title={getLocalizedText(label)} />
|
|
{isPartner && (
|
|
<Partner
|
|
className="ml-0.5 size-4"
|
|
text={t(($) => $['marketplace.partnerTip'], { ns: 'plugin' })}
|
|
/>
|
|
)}
|
|
{verified && (
|
|
<Verified
|
|
className="ml-0.5 size-4"
|
|
text={t(($) => $['marketplace.verifiedTip'], { ns: 'plugin' })}
|
|
/>
|
|
)}
|
|
{titleLeft} {/* This can be version badge */}
|
|
</div>
|
|
<OrgInfo className="mt-0.5" orgName={org} packageName={name} />
|
|
</div>
|
|
</div>
|
|
<Description
|
|
className={compact ? 'mt-1' : 'mt-3'}
|
|
text={getLocalizedText(brief)}
|
|
descriptionLineRows={effectiveDescriptionLineRows}
|
|
/>
|
|
{!!footer && <div>{footer}</div>}
|
|
</div>
|
|
{limitedInstall && (
|
|
<div className="relative flex h-8 items-center gap-x-2 px-3 after:absolute after:inset-0 after:bg-toast-warning-bg after:opacity-40">
|
|
<span
|
|
aria-hidden
|
|
className="i-ri-alert-fill size-3 shrink-0 text-text-warning-secondary"
|
|
/>
|
|
<p className="z-10 grow system-xs-regular text-text-secondary">
|
|
{t(($) => $['installModal.installWarning'], { ns: 'plugin' })}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default React.memo(Card)
|