AppCard.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. 'use client'
  2. import { useContext, useContextSelector } from 'use-context-selector'
  3. import { useRouter } from 'next/navigation'
  4. import { useCallback, useState } from 'react'
  5. import { useTranslation } from 'react-i18next'
  6. import cn from 'classnames'
  7. import s from './style.module.css'
  8. import type { App } from '@/types/app'
  9. import Confirm from '@/app/components/base/confirm'
  10. import { ToastContext } from '@/app/components/base/toast'
  11. import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
  12. import DuplicateAppModal from '@/app/components/app/duplicate-modal'
  13. import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
  14. import AppIcon from '@/app/components/base/app-icon'
  15. import AppsContext, { useAppContext } from '@/context/app-context'
  16. import type { HtmlContentProps } from '@/app/components/base/popover'
  17. import CustomPopover from '@/app/components/base/popover'
  18. import Divider from '@/app/components/base/divider'
  19. import { getRedirection } from '@/utils/app-redirection'
  20. import { useProviderContext } from '@/context/provider-context'
  21. import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
  22. import { AiText, ChatBot, CuteRobote } from '@/app/components/base/icons/src/vender/solid/communication'
  23. import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
  24. import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
  25. import EditAppModal from '@/app/components/explore/create-app-modal'
  26. import SwitchAppModal from '@/app/components/app/switch-app-modal'
  27. export type AppCardProps = {
  28. app: App
  29. onRefresh?: () => void
  30. }
  31. const AppCard = ({ app, onRefresh }: AppCardProps) => {
  32. const { t } = useTranslation()
  33. const { notify } = useContext(ToastContext)
  34. const { isCurrentWorkspaceManager } = useAppContext()
  35. const { onPlanInfoChanged } = useProviderContext()
  36. const { push } = useRouter()
  37. const mutateApps = useContextSelector(
  38. AppsContext,
  39. state => state.mutateApps,
  40. )
  41. const [showEditModal, setShowEditModal] = useState(false)
  42. const [showDuplicateModal, setShowDuplicateModal] = useState(false)
  43. const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
  44. const [showConfirmDelete, setShowConfirmDelete] = useState(false)
  45. const onConfirmDelete = useCallback(async () => {
  46. try {
  47. await deleteApp(app.id)
  48. notify({ type: 'success', message: t('app.appDeleted') })
  49. if (onRefresh)
  50. onRefresh()
  51. mutateApps()
  52. onPlanInfoChanged()
  53. }
  54. catch (e: any) {
  55. notify({
  56. type: 'error',
  57. message: `${t('app.appDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}`,
  58. })
  59. }
  60. setShowConfirmDelete(false)
  61. }, [app.id])
  62. const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
  63. name,
  64. icon,
  65. icon_background,
  66. description,
  67. }) => {
  68. try {
  69. await updateAppInfo({
  70. appID: app.id,
  71. name,
  72. icon,
  73. icon_background,
  74. description,
  75. })
  76. setShowEditModal(false)
  77. notify({
  78. type: 'success',
  79. message: t('app.editDone'),
  80. })
  81. if (onRefresh)
  82. onRefresh()
  83. mutateApps()
  84. }
  85. catch (e) {
  86. notify({ type: 'error', message: t('app.editFailed') })
  87. }
  88. }, [app.id, mutateApps, notify, onRefresh, t])
  89. const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon, icon_background }) => {
  90. try {
  91. const newApp = await copyApp({
  92. appID: app.id,
  93. name,
  94. icon,
  95. icon_background,
  96. mode: app.mode,
  97. })
  98. setShowDuplicateModal(false)
  99. notify({
  100. type: 'success',
  101. message: t('app.newApp.appCreated'),
  102. })
  103. localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
  104. if (onRefresh)
  105. onRefresh()
  106. mutateApps()
  107. onPlanInfoChanged()
  108. getRedirection(isCurrentWorkspaceManager, newApp, push)
  109. }
  110. catch (e) {
  111. notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
  112. }
  113. }
  114. const onExport = async () => {
  115. try {
  116. const { data } = await exportAppConfig(app.id)
  117. const a = document.createElement('a')
  118. const file = new Blob([data], { type: 'application/yaml' })
  119. a.href = URL.createObjectURL(file)
  120. a.download = `${app.name}.yml`
  121. a.click()
  122. }
  123. catch (e) {
  124. notify({ type: 'error', message: t('app.exportFailed') })
  125. }
  126. }
  127. const onSwitch = () => {
  128. if (onRefresh)
  129. onRefresh()
  130. mutateApps()
  131. setShowSwitchModal(false)
  132. }
  133. const Operations = (props: HtmlContentProps) => {
  134. const onClickSettings = async (e: React.MouseEvent<HTMLButtonElement>) => {
  135. e.stopPropagation()
  136. props.onClick?.()
  137. e.preventDefault()
  138. setShowEditModal(true)
  139. }
  140. const onClickDuplicate = async (e: React.MouseEvent<HTMLButtonElement>) => {
  141. e.stopPropagation()
  142. props.onClick?.()
  143. e.preventDefault()
  144. setShowDuplicateModal(true)
  145. }
  146. const onClickExport = async (e: React.MouseEvent<HTMLButtonElement>) => {
  147. e.stopPropagation()
  148. props.onClick?.()
  149. e.preventDefault()
  150. onExport()
  151. }
  152. const onClickSwitch = async (e: React.MouseEvent<HTMLDivElement>) => {
  153. e.stopPropagation()
  154. props.onClick?.()
  155. e.preventDefault()
  156. setShowSwitchModal(true)
  157. }
  158. const onClickDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
  159. e.stopPropagation()
  160. props.onClick?.()
  161. e.preventDefault()
  162. setShowConfirmDelete(true)
  163. }
  164. return (
  165. <div className="relative w-full py-1">
  166. <button className={s.actionItem} onClick={onClickSettings}>
  167. <span className={s.actionName}>{t('app.editApp')}</span>
  168. </button>
  169. <Divider className="!my-1" />
  170. <button className={s.actionItem} onClick={onClickDuplicate}>
  171. <span className={s.actionName}>{t('app.duplicate')}</span>
  172. </button>
  173. <button className={s.actionItem} onClick={onClickExport}>
  174. <span className={s.actionName}>{t('app.export')}</span>
  175. </button>
  176. {(app.mode === 'completion' || app.mode === 'chat') && (
  177. <>
  178. <Divider className="!my-1" />
  179. <div
  180. className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer'
  181. onClick={onClickSwitch}
  182. >
  183. <span className='text-gray-700 text-sm leading-5'>{t('app.switch')}</span>
  184. </div>
  185. </>
  186. )}
  187. <Divider className="!my-1" />
  188. <div
  189. className={cn(s.actionItem, s.deleteActionItem, 'group')}
  190. onClick={onClickDelete}
  191. >
  192. <span className={cn(s.actionName, 'group-hover:text-red-500')}>
  193. {t('common.operation.delete')}
  194. </span>
  195. </div>
  196. </div>
  197. )
  198. }
  199. return (
  200. <>
  201. <div
  202. onClick={(e) => {
  203. e.preventDefault()
  204. getRedirection(isCurrentWorkspaceManager, app, push)
  205. }}
  206. className='group flex col-span-1 bg-white border-2 border-solid border-transparent rounded-xl shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
  207. >
  208. <div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
  209. <div className='relative shrink-0'>
  210. <AppIcon
  211. size="large"
  212. icon={app.icon}
  213. background={app.icon_background}
  214. />
  215. <span className='absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'>
  216. {app.mode === 'advanced-chat' && (
  217. <ChatBot className='w-3 h-3 text-[#1570EF]' />
  218. )}
  219. {app.mode === 'agent-chat' && (
  220. <CuteRobote className='w-3 h-3 text-indigo-600' />
  221. )}
  222. {app.mode === 'chat' && (
  223. <ChatBot className='w-3 h-3 text-[#1570EF]' />
  224. )}
  225. {app.mode === 'completion' && (
  226. <AiText className='w-3 h-3 text-[#0E9384]' />
  227. )}
  228. {app.mode === 'workflow' && (
  229. <Route className='w-3 h-3 text-[#f79009]' />
  230. )}
  231. </span>
  232. </div>
  233. <div className='grow w-0 py-[1px]'>
  234. <div className='flex items-center text-sm leading-5 font-semibold text-gray-800'>
  235. <div className='truncate' title={app.name}>{app.name}</div>
  236. </div>
  237. <div className='flex items-center text-[10px] leading-[18px] text-gray-500 font-medium'>
  238. {app.mode === 'advanced-chat' && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>}
  239. {app.mode === 'chat' && <div className='truncate'>{t('app.types.chatbot').toUpperCase()}</div>}
  240. {app.mode === 'agent-chat' && <div className='truncate'>{t('app.types.agent').toUpperCase()}</div>}
  241. {app.mode === 'workflow' && <div className='truncate'>{t('app.types.workflow').toUpperCase()}</div>}
  242. {app.mode === 'completion' && <div className='truncate'>{t('app.types.completion').toUpperCase()}</div>}
  243. </div>
  244. </div>
  245. {isCurrentWorkspaceManager && <CustomPopover
  246. htmlContent={<Operations />}
  247. position="br"
  248. trigger="click"
  249. btnElement={<div className={cn(s.actionIcon, s.commonIcon)} />}
  250. btnClassName={open =>
  251. cn(
  252. open ? '!bg-gray-100 !shadow-none' : '!bg-transparent',
  253. '!hidden h-8 w-8 !p-2 rounded-md border-none hover:!bg-gray-100 group-hover:!inline-flex',
  254. )
  255. }
  256. className={'!w-[128px] h-fit !z-20'}
  257. popupClassName={
  258. (app.mode === 'completion' || app.mode === 'chat')
  259. ? '!w-[238px] translate-x-[-110px]'
  260. : ''
  261. }
  262. manualClose
  263. />}
  264. </div>
  265. <div className='mb-1 px-[14px] text-xs leading-normal text-gray-500 line-clamp-4'>{app.description}</div>
  266. {showEditModal && (
  267. <EditAppModal
  268. isEditModal
  269. appIcon={app.icon}
  270. appIconBackground={app.icon_background}
  271. appName={app.name}
  272. appDescription={app.description}
  273. show={showEditModal}
  274. onConfirm={onEdit}
  275. onHide={() => setShowEditModal(false)}
  276. />
  277. )}
  278. {showDuplicateModal && (
  279. <DuplicateAppModal
  280. appName={app.name}
  281. icon={app.icon}
  282. icon_background={app.icon_background}
  283. show={showDuplicateModal}
  284. onConfirm={onCopy}
  285. onHide={() => setShowDuplicateModal(false)}
  286. />
  287. )}
  288. {showSwitchModal && (
  289. <SwitchAppModal
  290. show={showSwitchModal}
  291. appDetail={app}
  292. onClose={() => setShowSwitchModal(false)}
  293. onSuccess={onSwitch}
  294. />
  295. )}
  296. {showConfirmDelete && (
  297. <Confirm
  298. title={t('app.deleteAppConfirmTitle')}
  299. content={t('app.deleteAppConfirmContent')}
  300. isShow={showConfirmDelete}
  301. onClose={() => setShowConfirmDelete(false)}
  302. onConfirm={onConfirmDelete}
  303. onCancel={() => setShowConfirmDelete(false)}
  304. />
  305. )}
  306. </div>
  307. </>
  308. )
  309. }
  310. export default AppCard