'use client' import { useContext, useContextSelector } from 'use-context-selector' import { useRouter } from 'next/navigation' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { RiMoreFill } from '@remixicon/react' import s from './style.module.css' import cn from '@/utils/classnames' import type { App } from '@/types/app' import Confirm from '@/app/components/base/confirm' import { ToastContext } from '@/app/components/base/toast' import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps' import DuplicateAppModal from '@/app/components/app/duplicate-modal' import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal' import AppIcon from '@/app/components/base/app-icon' import AppsContext, { useAppContext } from '@/context/app-context' import type { HtmlContentProps } from '@/app/components/base/popover' import CustomPopover from '@/app/components/base/popover' import Divider from '@/app/components/base/divider' import { getRedirection } from '@/utils/app-redirection' import { useProviderContext } from '@/context/provider-context' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { AiText, ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication' import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel' import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' import EditAppModal from '@/app/components/explore/create-app-modal' import SwitchAppModal from '@/app/components/app/switch-app-modal' import type { Tag } from '@/app/components/base/tag-management/constant' import TagSelector from '@/app/components/base/tag-management/selector' import type { EnvironmentVariable } from '@/app/components/workflow/types' import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal' import { fetchWorkflowDraft } from '@/service/workflow' export type AppCardProps = { app: App onRefresh?: () => void } const AppCard = ({ app, onRefresh }: AppCardProps) => { const { t } = useTranslation() const { notify } = useContext(ToastContext) const { isCurrentWorkspaceEditor } = useAppContext() const { onPlanInfoChanged } = useProviderContext() const { push } = useRouter() const mutateApps = useContextSelector( AppsContext, state => state.mutateApps, ) const [showEditModal, setShowEditModal] = useState(false) const [showDuplicateModal, setShowDuplicateModal] = useState(false) const [showSwitchModal, setShowSwitchModal] = useState(false) const [showConfirmDelete, setShowConfirmDelete] = useState(false) const [secretEnvList, setSecretEnvList] = useState([]) const onConfirmDelete = useCallback(async () => { try { await deleteApp(app.id) notify({ type: 'success', message: t('app.appDeleted') }) if (onRefresh) onRefresh() mutateApps() onPlanInfoChanged() } catch (e: any) { notify({ type: 'error', message: `${t('app.appDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}`, }) } setShowConfirmDelete(false) }, [app.id]) const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({ name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, }) => { try { await updateAppInfo({ appID: app.id, name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, }) setShowEditModal(false) notify({ type: 'success', message: t('app.editDone'), }) if (onRefresh) onRefresh() mutateApps() } catch (e) { notify({ type: 'error', message: t('app.editFailed') }) } }, [app.id, mutateApps, notify, onRefresh, t]) const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => { try { const newApp = await copyApp({ appID: app.id, name, icon_type, icon, icon_background, mode: app.mode, }) setShowDuplicateModal(false) notify({ type: 'success', message: t('app.newApp.appCreated'), }) localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') if (onRefresh) onRefresh() mutateApps() onPlanInfoChanged() getRedirection(isCurrentWorkspaceEditor, newApp, push) } catch (e) { notify({ type: 'error', message: t('app.newApp.appCreateFailed') }) } } const onExport = async (include = false) => { try { const { data } = await exportAppConfig({ appID: app.id, include, }) const a = document.createElement('a') const file = new Blob([data], { type: 'application/yaml' }) a.href = URL.createObjectURL(file) a.download = `${app.name}.yml` a.click() } catch (e) { notify({ type: 'error', message: t('app.exportFailed') }) } } const exportCheck = async () => { if (app.mode !== 'workflow' && app.mode !== 'advanced-chat') { onExport() return } try { const workflowDraft = await fetchWorkflowDraft(`/apps/${app.id}/workflows/draft`) const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret') if (list.length === 0) { onExport() return } setSecretEnvList(list) } catch (e) { notify({ type: 'error', message: t('app.exportFailed') }) } } const onSwitch = () => { if (onRefresh) onRefresh() mutateApps() setShowSwitchModal(false) } const Operations = (props: HtmlContentProps) => { const onMouseLeave = async () => { props.onClose?.() } const onClickSettings = async (e: React.MouseEvent) => { e.stopPropagation() props.onClick?.() e.preventDefault() setShowEditModal(true) } const onClickDuplicate = async (e: React.MouseEvent) => { e.stopPropagation() props.onClick?.() e.preventDefault() setShowDuplicateModal(true) } const onClickExport = async (e: React.MouseEvent) => { e.stopPropagation() props.onClick?.() e.preventDefault() exportCheck() } const onClickSwitch = async (e: React.MouseEvent) => { e.stopPropagation() props.onClick?.() e.preventDefault() setShowSwitchModal(true) } const onClickDelete = async (e: React.MouseEvent) => { e.stopPropagation() props.onClick?.() e.preventDefault() setShowConfirmDelete(true) } return (
{(app.mode === 'completion' || app.mode === 'chat') && ( <>
{t('app.switch')}
)}
{t('common.operation.delete')}
) } const [tags, setTags] = useState(app.tags) useEffect(() => { setTags(app.tags) }, [app.tags]) return ( <>
{ e.preventDefault() getRedirection(isCurrentWorkspaceEditor, app, push) }} className='relative group col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg' >
{app.mode === 'advanced-chat' && ( )} {app.mode === 'agent-chat' && ( )} {app.mode === 'chat' && ( )} {app.mode === 'completion' && ( )} {app.mode === 'workflow' && ( )}
{app.name}
{app.mode === 'advanced-chat' &&
{t('app.types.chatbot').toUpperCase()}
} {app.mode === 'chat' &&
{t('app.types.chatbot').toUpperCase()}
} {app.mode === 'agent-chat' &&
{t('app.types.agent').toUpperCase()}
} {app.mode === 'workflow' &&
{t('app.types.workflow').toUpperCase()}
} {app.mode === 'completion' &&
{t('app.types.completion').toUpperCase()}
}
{app.description}
{isCurrentWorkspaceEditor && ( <>
{ e.stopPropagation() e.preventDefault() }}>
tag.id)} selectedTags={tags} onCacheUpdate={setTags} onChange={onRefresh} />
} position="br" trigger="click" btnElement={
} btnClassName={open => cn( open ? '!bg-black/5 !shadow-none' : '!bg-transparent', 'h-8 w-8 !p-2 rounded-md border-none hover:!bg-black/5', ) } popupClassName={ (app.mode === 'completion' || app.mode === 'chat') ? '!w-[238px] translate-x-[-110px]' : '' } className={'!w-[128px] h-fit !z-20'} />
)}
{showEditModal && ( setShowEditModal(false)} /> )} {showDuplicateModal && ( setShowDuplicateModal(false)} /> )} {showSwitchModal && ( setShowSwitchModal(false)} onSuccess={onSwitch} /> )} {showConfirmDelete && ( setShowConfirmDelete(false)} /> )} {secretEnvList.length > 0 && ( setSecretEnvList([])} /> )} ) } export default AppCard