app-info.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. import { useTranslation } from 'react-i18next'
  2. import { useRouter } from 'next/navigation'
  3. import { useContext, useContextSelector } from 'use-context-selector'
  4. import cn from 'classnames'
  5. import React, { useCallback, useState } from 'react'
  6. import AppIcon from '../base/app-icon'
  7. import SwitchAppModal from '../app/switch-app-modal'
  8. import s from './style.module.css'
  9. import {
  10. PortalToFollowElem,
  11. PortalToFollowElemContent,
  12. PortalToFollowElemTrigger,
  13. } from '@/app/components/base/portal-to-follow-elem'
  14. import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
  15. import Divider from '@/app/components/base/divider'
  16. import Confirm from '@/app/components/base/confirm'
  17. import { useStore as useAppStore } from '@/app/components/app/store'
  18. import { ToastContext } from '@/app/components/base/toast'
  19. import AppsContext from '@/context/app-context'
  20. import { useProviderContext } from '@/context/provider-context'
  21. import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
  22. import DuplicateAppModal from '@/app/components/app/duplicate-modal'
  23. import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
  24. import CreateAppModal from '@/app/components/explore/create-app-modal'
  25. import { AiText, ChatBot, CuteRobote } from '@/app/components/base/icons/src/vender/solid/communication'
  26. import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
  27. import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
  28. import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
  29. import { getRedirection } from '@/utils/app-redirection'
  30. export type IAppInfoProps = {
  31. expand: boolean
  32. }
  33. const AppInfo = ({ expand }: IAppInfoProps) => {
  34. const { t } = useTranslation()
  35. const { notify } = useContext(ToastContext)
  36. const { replace } = useRouter()
  37. const { onPlanInfoChanged } = useProviderContext()
  38. const appDetail = useAppStore(state => state.appDetail)
  39. const setAppDetail = useAppStore(state => state.setAppDetail)
  40. const [open, setOpen] = useState(false)
  41. const [showEditModal, setShowEditModal] = useState(false)
  42. const [showDuplicateModal, setShowDuplicateModal] = useState(false)
  43. const [showConfirmDelete, setShowConfirmDelete] = useState(false)
  44. const [showSwitchTip, setShowSwitchTip] = useState<string>('')
  45. const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
  46. const mutateApps = useContextSelector(
  47. AppsContext,
  48. state => state.mutateApps,
  49. )
  50. const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
  51. name,
  52. icon,
  53. icon_background,
  54. description,
  55. }) => {
  56. if (!appDetail)
  57. return
  58. try {
  59. const app = await updateAppInfo({
  60. appID: appDetail.id,
  61. name,
  62. icon,
  63. icon_background,
  64. description,
  65. })
  66. setShowEditModal(false)
  67. notify({
  68. type: 'success',
  69. message: t('app.editDone'),
  70. })
  71. setAppDetail(app)
  72. mutateApps()
  73. }
  74. catch (e) {
  75. notify({ type: 'error', message: t('app.editFailed') })
  76. }
  77. }, [appDetail, mutateApps, notify, setAppDetail, t])
  78. const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon, icon_background }) => {
  79. if (!appDetail)
  80. return
  81. try {
  82. const newApp = await copyApp({
  83. appID: appDetail.id,
  84. name,
  85. icon,
  86. icon_background,
  87. mode: appDetail.mode,
  88. })
  89. setShowDuplicateModal(false)
  90. notify({
  91. type: 'success',
  92. message: t('app.newApp.appCreated'),
  93. })
  94. localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
  95. mutateApps()
  96. onPlanInfoChanged()
  97. getRedirection(true, newApp, replace)
  98. }
  99. catch (e) {
  100. notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
  101. }
  102. }
  103. const onExport = async () => {
  104. if (!appDetail)
  105. return
  106. try {
  107. const { data } = await exportAppConfig(appDetail.id)
  108. const a = document.createElement('a')
  109. const file = new Blob([data], { type: 'application/yaml' })
  110. a.href = URL.createObjectURL(file)
  111. a.download = `${appDetail.name}.yml`
  112. a.click()
  113. }
  114. catch (e) {
  115. notify({ type: 'error', message: t('app.exportFailed') })
  116. }
  117. }
  118. const onConfirmDelete = useCallback(async () => {
  119. if (!appDetail)
  120. return
  121. try {
  122. await deleteApp(appDetail.id)
  123. notify({ type: 'success', message: t('app.appDeleted') })
  124. mutateApps()
  125. onPlanInfoChanged()
  126. setAppDetail()
  127. replace('/apps')
  128. }
  129. catch (e: any) {
  130. notify({
  131. type: 'error',
  132. message: `${t('app.appDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}`,
  133. })
  134. }
  135. setShowConfirmDelete(false)
  136. }, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, t])
  137. if (!appDetail)
  138. return null
  139. return (
  140. <PortalToFollowElem
  141. open={open}
  142. onOpenChange={setOpen}
  143. placement='bottom-start'
  144. offset={4}
  145. >
  146. <div className='relative'>
  147. <PortalToFollowElemTrigger
  148. onClick={() => setOpen(v => !v)}
  149. className='block'
  150. >
  151. <div className={cn('flex cursor-pointer p-1 rounded-lg hover:bg-gray-100', open && 'bg-gray-100')}>
  152. <div className='relative shrink-0 mr-2'>
  153. <AppIcon size={expand ? 'large' : 'small'} icon={appDetail.icon} background={appDetail.icon_background} />
  154. <span className={cn(
  155. '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',
  156. !expand && '!w-3.5 !h-3.5 !bottom-[-2px] !right-[-2px]',
  157. )}>
  158. {appDetail.mode === 'advanced-chat' && (
  159. <ChatBot className={cn('w-3 h-3 text-[#1570EF]', !expand && '!w-2.5 !h-2.5')} />
  160. )}
  161. {appDetail.mode === 'agent-chat' && (
  162. <CuteRobote className={cn('w-3 h-3 text-indigo-600', !expand && '!w-2.5 !h-2.5')} />
  163. )}
  164. {appDetail.mode === 'chat' && (
  165. <ChatBot className={cn('w-3 h-3 text-[#1570EF]', !expand && '!w-2.5 !h-2.5')} />
  166. )}
  167. {appDetail.mode === 'completion' && (
  168. <AiText className={cn('w-3 h-3 text-[#0E9384]', !expand && '!w-2.5 !h-2.5')} />
  169. )}
  170. {appDetail.mode === 'workflow' && (
  171. <Route className={cn('w-3 h-3 text-[#f79009]', !expand && '!w-2.5 !h-2.5')} />
  172. )}
  173. </span>
  174. </div>
  175. {expand && (
  176. <div className="grow w-0">
  177. <div className='flex justify-between items-center text-sm leading-5 font-medium text-gray-900'>
  178. <div className='truncate' title={appDetail.name}>{appDetail.name}</div>
  179. <ChevronDown className='shrink-0 ml-[2px] w-3 h-3 text-gray-500' />
  180. </div>
  181. <div className='flex items-center text-[10px] leading-[18px] font-medium text-gray-500 gap-1'>
  182. {appDetail.mode === 'advanced-chat' && (
  183. <>
  184. <div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
  185. <div title={t('app.newApp.advanced') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.newApp.advanced').toUpperCase()}</div>
  186. </>
  187. )}
  188. {appDetail.mode === 'agent-chat' && (
  189. <div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.agent').toUpperCase()}</div>
  190. )}
  191. {appDetail.mode === 'chat' && (
  192. <>
  193. <div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
  194. <div title={t('app.newApp.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.newApp.basic').toUpperCase())}</div>
  195. </>
  196. )}
  197. {appDetail.mode === 'completion' && (
  198. <>
  199. <div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.completion').toUpperCase()}</div>
  200. <div title={t('app.newApp.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.newApp.basic').toUpperCase())}</div>
  201. </>
  202. )}
  203. {appDetail.mode === 'workflow' && (
  204. <div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.workflow').toUpperCase()}</div>
  205. )}
  206. </div>
  207. </div>
  208. )}
  209. </div>
  210. </PortalToFollowElemTrigger>
  211. <PortalToFollowElemContent className='z-[1002]'>
  212. <div className='relative w-[320px] bg-white rounded-2xl shadow-xl'>
  213. {/* header */}
  214. <div className={cn('flex pl-4 pt-3 pr-3', !appDetail.description && 'pb-2')}>
  215. <div className='relative shrink-0 mr-2'>
  216. <AppIcon size="large" icon={appDetail.icon} background={appDetail.icon_background} />
  217. <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'>
  218. {appDetail.mode === 'advanced-chat' && (
  219. <ChatBot className='w-3 h-3 text-[#1570EF]' />
  220. )}
  221. {appDetail.mode === 'agent-chat' && (
  222. <CuteRobote className='w-3 h-3 text-indigo-600' />
  223. )}
  224. {appDetail.mode === 'chat' && (
  225. <ChatBot className='w-3 h-3 text-[#1570EF]' />
  226. )}
  227. {appDetail.mode === 'completion' && (
  228. <AiText className='w-3 h-3 text-[#0E9384]' />
  229. )}
  230. {appDetail.mode === 'workflow' && (
  231. <Route className='w-3 h-3 text-[#f79009]' />
  232. )}
  233. </span>
  234. </div>
  235. <div className='grow w-0'>
  236. <div title={appDetail.name} className='flex justify-between items-center text-sm leading-5 font-medium text-gray-900 truncate'>{appDetail.name}</div>
  237. <div className='flex items-center text-[10px] leading-[18px] font-medium text-gray-500 gap-1'>
  238. {appDetail.mode === 'advanced-chat' && (
  239. <>
  240. <div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
  241. <div title={t('app.newApp.advanced') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.newApp.advanced').toUpperCase()}</div>
  242. </>
  243. )}
  244. {appDetail.mode === 'agent-chat' && (
  245. <div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.agent').toUpperCase()}</div>
  246. )}
  247. {appDetail.mode === 'chat' && (
  248. <>
  249. <div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
  250. <div title={t('app.newApp.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.newApp.basic').toUpperCase())}</div>
  251. </>
  252. )}
  253. {appDetail.mode === 'completion' && (
  254. <>
  255. <div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.completion').toUpperCase()}</div>
  256. <div title={t('app.newApp.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.newApp.basic').toUpperCase())}</div>
  257. </>
  258. )}
  259. {appDetail.mode === 'workflow' && (
  260. <div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.workflow').toUpperCase()}</div>
  261. )}
  262. </div>
  263. </div>
  264. </div>
  265. {/* desscription */}
  266. {appDetail.description && (
  267. <div className='px-4 py-2 text-gray-500 text-xs leading-[18px]'>{appDetail.description}</div>
  268. )}
  269. {/* operations */}
  270. <Divider className="!my-1" />
  271. <div className="w-full py-1">
  272. <div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={() => {
  273. setOpen(false)
  274. setShowEditModal(true)
  275. }}>
  276. <span className='text-gray-700 text-sm leading-5'>{t('app.editApp')}</span>
  277. </div>
  278. <div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={() => {
  279. setOpen(false)
  280. setShowDuplicateModal(true)
  281. }}>
  282. <span className='text-gray-700 text-sm leading-5'>{t('app.duplicate')}</span>
  283. </div>
  284. <div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={onExport}>
  285. <span className='text-gray-700 text-sm leading-5'>{t('app.export')}</span>
  286. </div>
  287. {(appDetail.mode === 'completion' || appDetail.mode === 'chat') && (
  288. <>
  289. <Divider className="!my-1" />
  290. <div
  291. className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer'
  292. onMouseEnter={() => setShowSwitchTip(appDetail.mode)}
  293. onMouseLeave={() => setShowSwitchTip('')}
  294. onClick={() => {
  295. setOpen(false)
  296. setShowSwitchModal(true)
  297. }}
  298. >
  299. <span className='text-gray-700 text-sm leading-5'>{t('app.switch')}</span>
  300. </div>
  301. </>
  302. )}
  303. <Divider className="!my-1" />
  304. <div className='group h-9 py-2 px-3 mx-1 flex items-center hover:bg-red-50 rounded-lg cursor-pointer' onClick={() => {
  305. setOpen(false)
  306. setShowConfirmDelete(true)
  307. }}>
  308. <span className='text-gray-700 text-sm leading-5 group-hover:text-red-500'>
  309. {t('common.operation.delete')}
  310. </span>
  311. </div>
  312. </div>
  313. {/* switch tip */}
  314. <div
  315. className={cn(
  316. 'hidden absolute left-[324px] top-0 w-[376px] rounded-xl bg-white border-[0.5px] border-[rgba(0,0,0,0.05)] shadow-lg',
  317. showSwitchTip && '!block',
  318. )}
  319. >
  320. <div className={cn(
  321. 'w-full h-[256px] bg-center bg-no-repeat bg-contain rounded-xl',
  322. showSwitchTip === 'chat' && s.expertPic,
  323. showSwitchTip === 'completion' && s.completionPic,
  324. )}/>
  325. <div className='px-4 pb-2'>
  326. <div className='flex items-center gap-1 text-gray-700 text-md leading-6 font-semibold'>
  327. {showSwitchTip === 'chat' ? t('app.newApp.advanced') : t('app.types.workflow')}
  328. <span className='px-1 rounded-[5px] bg-white border border-black/8 text-gray-500 text-[10px] leading-[18px] font-medium'>BETA</span>
  329. </div>
  330. <div className='text-orange-500 text-xs leading-[18px] font-medium'>{t('app.newApp.advancedFor').toLocaleUpperCase()}</div>
  331. <div className='mt-1 text-gray-500 text-sm leading-5'>{t('app.newApp.advancedDescription')}</div>
  332. </div>
  333. </div>
  334. </div>
  335. </PortalToFollowElemContent>
  336. {showSwitchModal && (
  337. <SwitchAppModal
  338. inAppDetail
  339. show={showSwitchModal}
  340. appDetail={appDetail}
  341. onClose={() => setShowSwitchModal(false)}
  342. onSuccess={() => setShowSwitchModal(false)}
  343. />
  344. )}
  345. {showEditModal && (
  346. <CreateAppModal
  347. isEditModal
  348. appIcon={appDetail.icon}
  349. appIconBackground={appDetail.icon_background}
  350. appName={appDetail.name}
  351. appDescription={appDetail.description}
  352. show={showEditModal}
  353. onConfirm={onEdit}
  354. onHide={() => setShowEditModal(false)}
  355. />
  356. )}
  357. {showDuplicateModal && (
  358. <DuplicateAppModal
  359. appName={appDetail.name}
  360. icon={appDetail.icon}
  361. icon_background={appDetail.icon_background}
  362. show={showDuplicateModal}
  363. onConfirm={onCopy}
  364. onHide={() => setShowDuplicateModal(false)}
  365. />
  366. )}
  367. {showConfirmDelete && (
  368. <Confirm
  369. title={t('app.deleteAppConfirmTitle')}
  370. content={t('app.deleteAppConfirmContent')}
  371. isShow={showConfirmDelete}
  372. onClose={() => setShowConfirmDelete(false)}
  373. onConfirm={onConfirmDelete}
  374. onCancel={() => setShowConfirmDelete(false)}
  375. />
  376. )}
  377. </div>
  378. </PortalToFollowElem>
  379. )
  380. }
  381. export default React.memo(AppInfo)