'use client' import type { AppPartial } from '@dify/contracts/api/console/apps/types.gen' import type { WorkflowOnlineUser } from '@/models/app' import { zIconType } from '@dify/contracts/api/console/apps/zod.gen' import { cn } from '@langgenius/dify-ui/cn' import { toast } from '@langgenius/dify-ui/toast' import { useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { memo, useCallback, useId, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { AppTypeIcon } from '@/app/components/app/type-selector' import AppIcon from '@/app/components/base/app-icon' import { UserAvatarList } from '@/app/components/base/user-avatar-list' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { userProfileQueryOptions } from '@/features/account-profile/client' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { AppCardTags } from '@/features/tag-management/components/app-card-tags' import Link from '@/next/link' import { AppModeEnum } from '@/types/app' import { getRedirectionPath } from '@/utils/app-redirection' import { getAppACLCapabilities, hasOnlyAppPreviewPermission, hasPermission, } from '@/utils/permission' import { formatTime } from '@/utils/time' import { AppCardInteractions } from './interactions' const EMPTY_ONLINE_USERS: WorkflowOnlineUser[] = [] type AppCardProps = { app: AppPartial onlineUsers?: WorkflowOnlineUser[] onOpenTagManagement?: () => void stepByStepTourActionMenuOpen?: boolean stepByStepTourCardTarget?: string stepByStepTourCardHighlightPart?: string stepByStepTourActionMenuHighlightPart?: string } export const AppCard = memo( ({ app, onlineUsers = EMPTY_ONLINE_USERS, onOpenTagManagement, stepByStepTourActionMenuOpen = false, stepByStepTourCardTarget, stepByStepTourCardHighlightPart, stepByStepTourActionMenuHighlightPart, }: AppCardProps) => { const { t } = useTranslation() const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const { data: currentUserId } = useSuspenseQuery({ ...userProfileQueryOptions(), select: (data) => data.profile.id, }) const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) const isRbacEnabled = systemFeatures.rbac_enabled const resourceMaintainer = app.maintainer ?? undefined const maintainerPermissionOptions = useMemo( () => ({ currentUserId, resourceMaintainer, workspacePermissionKeys, isRbacEnabled, }), [currentUserId, isRbacEnabled, resourceMaintainer, workspacePermissionKeys], ) const appACLCapabilities = useMemo( () => getAppACLCapabilities(app.permission_keys, maintainerPermissionOptions), [app.permission_keys, maintainerPermissionOptions], ) const isPreviewOnly = hasOnlyAppPreviewPermission(app.permission_keys) const canManageAppTags = hasPermission(workspacePermissionKeys, 'app.tag.manage') const canBindOrUnbindTags = !isPreviewOnly && (canManageAppTags || appACLCapabilities.canEdit) const editTimeText = useMemo(() => { const timestamp = app.updated_at || app.created_at if (!timestamp) return '' const timeText = formatTime({ date: timestamp * 1000, dateFormat: `${t(($) => $['segment.dateTimeFormat'], { ns: 'datasetDocuments' })}`, }) return `${t(($) => $['segment.editedAt'], { ns: 'datasetDocuments' })} ${timeText}` }, [app.updated_at, app.created_at, t]) const appModeLabel = useMemo(() => { switch (app.mode) { case AppModeEnum.CHAT: return t(($) => $['types.chatbot'], { ns: 'app' }) case AppModeEnum.ADVANCED_CHAT: return t(($) => $['types.advanced'], { ns: 'app' }) case AppModeEnum.AGENT_CHAT: return t(($) => $['types.agent'], { ns: 'app' }) case AppModeEnum.COMPLETION: return t(($) => $['types.completion'], { ns: 'app' }) case AppModeEnum.WORKFLOW: return t(($) => $['types.workflow'], { ns: 'app' }) default: return app.mode } }, [app.mode, t]) const onlinePresenceUsers = useMemo(() => { return onlineUsers .map((user, index) => { const id = user.user_id || user.sid || `${app.id}-online-${index}` const name = user.username || user.user_id || user.sid || `${index + 1}` return { id, name, avatar_url: user.avatar || null, } }) .filter((user) => Boolean(user.id)) }, [app.id, onlineUsers]) const appNameId = useId() const appDescriptionId = useId() const appIconType = zIconType.safeParse(app.icon_type).data ?? null const appHref = getRedirectionPath(app, maintainerPermissionOptions) const appCardClassName = cn( 'inline-flex h-full w-full touch-manipulation flex-col rounded-xl outline-hidden', isPreviewOnly ? 'cursor-not-allowed opacity-60' : 'cursor-pointer', ) const showPreviewOnlyAccessWarning = useCallback(() => { toast.warning(t(($) => $.noAccessResourcePermission, { ns: 'app' })) }, [t]) const appCardContent = ( <>