import type { Operation } from './app-operations' import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal' import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' import type { EnvironmentVariable } from '@/app/components/workflow/types' import { RiDeleteBinLine, RiEditLine, RiEqualizer2Line, RiExchange2Line, RiFileCopy2Line, RiFileDownloadLine, RiFileUploadLine, } from '@remixicon/react' import dynamic from 'next/dynamic' import { useRouter } from 'next/navigation' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view' import { useStore as useAppStore } from '@/app/components/app/store' import Button from '@/app/components/base/button' import ContentDialog from '@/app/components/base/content-dialog' import { ToastContext } from '@/app/components/base/toast' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' import { useProviderContext } from '@/context/provider-context' import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps' import { useInvalidateAppList } from '@/service/use-apps' import { fetchWorkflowDraft } from '@/service/workflow' import { AppModeEnum } from '@/types/app' import { getRedirection } from '@/utils/app-redirection' import { cn } from '@/utils/classnames' import AppIcon from '../base/app-icon' import AppOperations from './app-operations' const SwitchAppModal = dynamic(() => import('@/app/components/app/switch-app-modal'), { ssr: false, }) const CreateAppModal = dynamic(() => import('@/app/components/explore/create-app-modal'), { ssr: false, }) const DuplicateAppModal = dynamic(() => import('@/app/components/app/duplicate-modal'), { ssr: false, }) const Confirm = dynamic(() => import('@/app/components/base/confirm'), { ssr: false, }) const UpdateDSLModal = dynamic(() => import('@/app/components/workflow/update-dsl-modal'), { ssr: false, }) const DSLExportConfirmModal = dynamic(() => import('@/app/components/workflow/dsl-export-confirm-modal'), { ssr: false, }) export type IAppInfoProps = { expand: boolean onlyShowDetail?: boolean openState?: boolean onDetailExpand?: (expand: boolean) => void } const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailExpand }: IAppInfoProps) => { const { t } = useTranslation() const { notify } = useContext(ToastContext) const { replace } = useRouter() const { onPlanInfoChanged } = useProviderContext() const appDetail = useAppStore(state => state.appDetail) const setAppDetail = useAppStore(state => state.setAppDetail) const invalidateAppList = useInvalidateAppList() const [open, setOpen] = useState(openState) const [showEditModal, setShowEditModal] = useState(false) const [showDuplicateModal, setShowDuplicateModal] = useState(false) const [showConfirmDelete, setShowConfirmDelete] = useState(false) const [showSwitchModal, setShowSwitchModal] = useState(false) const [showImportDSLModal, setShowImportDSLModal] = useState(false) const [secretEnvList, setSecretEnvList] = useState([]) const [showExportWarning, setShowExportWarning] = useState(false) const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({ name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, max_active_requests, }) => { if (!appDetail) return try { const app = await updateAppInfo({ appID: appDetail.id, name, icon_type, icon, icon_background, description, use_icon_as_answer_icon, max_active_requests, }) setShowEditModal(false) notify({ type: 'success', message: t('editDone', { ns: 'app' }), }) setAppDetail(app) } catch { notify({ type: 'error', message: t('editFailed', { ns: 'app' }) }) } }, [appDetail, notify, setAppDetail, t]) const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon_type, icon, icon_background }) => { if (!appDetail) return try { const newApp = await copyApp({ appID: appDetail.id, name, icon_type, icon, icon_background, mode: appDetail.mode, }) setShowDuplicateModal(false) notify({ type: 'success', message: t('newApp.appCreated', { ns: 'app' }), }) localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') onPlanInfoChanged() getRedirection(true, newApp, replace) } catch { notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) }) } } const onExport = async (include = false) => { if (!appDetail) return try { const { data } = await exportAppConfig({ appID: appDetail.id, include, }) const a = document.createElement('a') const file = new Blob([data], { type: 'application/yaml' }) const url = URL.createObjectURL(file) a.href = url a.download = `${appDetail.name}.yml` a.click() URL.revokeObjectURL(url) } catch { notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) }) } } const exportCheck = async () => { if (!appDetail) return if (appDetail.mode !== AppModeEnum.WORKFLOW && appDetail.mode !== AppModeEnum.ADVANCED_CHAT) { onExport() return } setShowExportWarning(true) } const handleConfirmExport = async () => { if (!appDetail) return setShowExportWarning(false) try { const workflowDraft = await fetchWorkflowDraft(`/apps/${appDetail.id}/workflows/draft`) const list = (workflowDraft.environment_variables || []).filter(env => env.value_type === 'secret') if (list.length === 0) { onExport() return } setSecretEnvList(list) } catch { notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) }) } } const onConfirmDelete = useCallback(async () => { if (!appDetail) return try { await deleteApp(appDetail.id) notify({ type: 'success', message: t('appDeleted', { ns: 'app' }) }) invalidateAppList() onPlanInfoChanged() setAppDetail() replace('/apps') } catch (e: any) { notify({ type: 'error', message: `${t('appDeleteFailed', { ns: 'app' })}${'message' in e ? `: ${e.message}` : ''}`, }) } setShowConfirmDelete(false) }, [appDetail, invalidateAppList, notify, onPlanInfoChanged, replace, setAppDetail, t]) const { isCurrentWorkspaceEditor } = useAppContext() if (!appDetail) return null const primaryOperations = [ { id: 'edit', title: t('editApp', { ns: 'app' }), icon: , onClick: () => { setOpen(false) onDetailExpand?.(false) setShowEditModal(true) }, }, { id: 'duplicate', title: t('duplicate', { ns: 'app' }), icon: , onClick: () => { setOpen(false) onDetailExpand?.(false) setShowDuplicateModal(true) }, }, { id: 'export', title: t('export', { ns: 'app' }), icon: , onClick: exportCheck, }, ] const secondaryOperations: Operation[] = [ // Import DSL (conditional) ...(appDetail.mode === AppModeEnum.ADVANCED_CHAT || appDetail.mode === AppModeEnum.WORKFLOW) ? [{ id: 'import', title: t('common.importDSL', { ns: 'workflow' }), icon: , onClick: () => { setOpen(false) onDetailExpand?.(false) setShowImportDSLModal(true) }, }] : [], // Divider { id: 'divider-1', title: '', icon: <>, onClick: () => { /* divider has no action */ }, type: 'divider' as const, }, // Delete operation { id: 'delete', title: t('operation.delete', { ns: 'common' }), icon: , onClick: () => { setOpen(false) onDetailExpand?.(false) setShowConfirmDelete(true) }, }, ] // Keep the switch operation separate as it's not part of the main operations const switchOperation = (appDetail.mode === AppModeEnum.COMPLETION || appDetail.mode === AppModeEnum.CHAT) ? { id: 'switch', title: t('switch', { ns: 'app' }), icon: , onClick: () => { setOpen(false) onDetailExpand?.(false) setShowSwitchModal(true) }, } : null return (
{!onlyShowDetail && ( )} { setOpen(false) onDetailExpand?.(false) }} className="absolute bottom-2 left-2 top-2 flex w-[420px] flex-col rounded-2xl !p-0" >
{appDetail.name}
{appDetail.mode === AppModeEnum.ADVANCED_CHAT ? t('types.advanced', { ns: 'app' }) : appDetail.mode === AppModeEnum.AGENT_CHAT ? t('types.agent', { ns: 'app' }) : appDetail.mode === AppModeEnum.CHAT ? t('types.chatbot', { ns: 'app' }) : appDetail.mode === AppModeEnum.COMPLETION ? t('types.completion', { ns: 'app' }) : t('types.workflow', { ns: 'app' })}
{/* description */} {appDetail.description && (
{appDetail.description}
)} {/* operations */}
{/* Switch operation (if available) */} {switchOperation && (
)}
{showSwitchModal && ( setShowSwitchModal(false)} onSuccess={() => setShowSwitchModal(false)} /> )} {showEditModal && ( setShowEditModal(false)} /> )} {showDuplicateModal && ( setShowDuplicateModal(false)} /> )} {showConfirmDelete && ( setShowConfirmDelete(false)} /> )} {showImportDSLModal && ( setShowImportDSLModal(false)} onBackup={exportCheck} /> )} {secretEnvList.length > 0 && ( setSecretEnvList([])} /> )} {showExportWarning && ( setShowExportWarning(false)} /> )}
) } export default React.memo(AppInfo)