Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

index.tsx 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. 'use client'
  2. import { useCallback, useEffect, useRef, useState } from 'react'
  3. import { useTranslation } from 'react-i18next'
  4. import { useRouter, useSearchParams } from 'next/navigation'
  5. import { useContext, useContextSelector } from 'use-context-selector'
  6. import { RiArrowRightLine, RiCommandLine, RiCornerDownLeftLine, RiExchange2Fill } from '@remixicon/react'
  7. import Link from 'next/link'
  8. import { useDebounceFn, useKeyPress } from 'ahooks'
  9. import Image from 'next/image'
  10. import AppIconPicker from '../../base/app-icon-picker'
  11. import type { AppIconSelection } from '../../base/app-icon-picker'
  12. import Button from '@/app/components/base/button'
  13. import Divider from '@/app/components/base/divider'
  14. import cn from '@/utils/classnames'
  15. import { basePath } from '@/utils/var'
  16. import AppsContext, { useAppContext } from '@/context/app-context'
  17. import { useProviderContext } from '@/context/provider-context'
  18. import { ToastContext } from '@/app/components/base/toast'
  19. import type { AppMode } from '@/types/app'
  20. import { AppModes } from '@/types/app'
  21. import { createApp } from '@/service/apps'
  22. import Input from '@/app/components/base/input'
  23. import Textarea from '@/app/components/base/textarea'
  24. import AppIcon from '@/app/components/base/app-icon'
  25. import AppsFull from '@/app/components/billing/apps-full-in-dialog'
  26. import { BubbleTextMod, ChatBot, ListSparkle, Logic } from '@/app/components/base/icons/src/vender/solid/communication'
  27. import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
  28. import { getRedirection } from '@/utils/app-redirection'
  29. import FullScreenModal from '@/app/components/base/fullscreen-modal'
  30. import useTheme from '@/hooks/use-theme'
  31. type CreateAppProps = {
  32. onSuccess: () => void
  33. onClose: () => void
  34. onCreateFromTemplate?: () => void
  35. }
  36. function CreateApp({ onClose, onSuccess, onCreateFromTemplate }: CreateAppProps) {
  37. const { t } = useTranslation()
  38. const { push } = useRouter()
  39. const { notify } = useContext(ToastContext)
  40. const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
  41. const [appMode, setAppMode] = useState<AppMode>('chat')
  42. const [appIcon, setAppIcon] = useState<AppIconSelection>({ type: 'emoji', icon: '🤖', background: '#FFEAD5' })
  43. const [showAppIconPicker, setShowAppIconPicker] = useState(false)
  44. const [name, setName] = useState('')
  45. const [description, setDescription] = useState('')
  46. const { plan, enableBilling } = useProviderContext()
  47. const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
  48. const { isCurrentWorkspaceEditor } = useAppContext()
  49. const isCreatingRef = useRef(false)
  50. const searchParams = useSearchParams()
  51. useEffect(() => {
  52. const category = searchParams.get('category')
  53. if (category && AppModes.includes(category as AppMode))
  54. setAppMode(category as AppMode)
  55. }, [searchParams])
  56. const onCreate = useCallback(async () => {
  57. if (!appMode) {
  58. notify({ type: 'error', message: t('app.newApp.appTypeRequired') })
  59. return
  60. }
  61. if (!name.trim()) {
  62. notify({ type: 'error', message: t('app.newApp.nameNotEmpty') })
  63. return
  64. }
  65. if (isCreatingRef.current)
  66. return
  67. isCreatingRef.current = true
  68. try {
  69. const app = await createApp({
  70. name,
  71. description,
  72. icon_type: appIcon.type,
  73. icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId,
  74. icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined,
  75. mode: appMode,
  76. })
  77. notify({ type: 'success', message: t('app.newApp.appCreated') })
  78. onSuccess()
  79. onClose()
  80. mutateApps()
  81. localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
  82. getRedirection(isCurrentWorkspaceEditor, app, push)
  83. }
  84. catch {
  85. notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
  86. }
  87. isCreatingRef.current = false
  88. }, [name, notify, t, appMode, appIcon, description, onSuccess, onClose, mutateApps, push, isCurrentWorkspaceEditor])
  89. const { run: handleCreateApp } = useDebounceFn(onCreate, { wait: 300 })
  90. useKeyPress(['meta.enter', 'ctrl.enter'], () => {
  91. if (isAppsFull)
  92. return
  93. handleCreateApp()
  94. })
  95. return <>
  96. <div className='flex h-full justify-center overflow-y-auto overflow-x-hidden'>
  97. <div className='flex flex-1 shrink-0 justify-end'>
  98. <div className='px-10'>
  99. <div className='h-6 w-full 2xl:h-[139px]' />
  100. <div className='pb-6 pt-1'>
  101. <span className='title-2xl-semi-bold text-text-primary'>{t('app.newApp.startFromBlank')}</span>
  102. </div>
  103. <div className='mb-2 leading-6'>
  104. <span className='system-sm-semibold text-text-secondary'>{t('app.newApp.chooseAppType')}</span>
  105. </div>
  106. <div className='flex w-[660px] flex-col gap-4'>
  107. <div>
  108. <div className='mb-2'>
  109. <span className='system-2xs-medium-uppercase text-text-tertiary'>{t('app.newApp.forBeginners')}</span>
  110. </div>
  111. <div className='flex flex-row gap-2'>
  112. <AppTypeCard
  113. active={appMode === 'chat'}
  114. title={t('app.types.chatbot')}
  115. description={t('app.newApp.chatbotShortDescription')}
  116. icon={<div className='flex h-6 w-6 items-center justify-center rounded-md bg-components-icon-bg-blue-solid'>
  117. <ChatBot className='h-4 w-4 text-components-avatar-shape-fill-stop-100' />
  118. </div>}
  119. onClick={() => {
  120. setAppMode('chat')
  121. }} />
  122. <AppTypeCard
  123. active={appMode === 'agent-chat'}
  124. title={t('app.types.agent')}
  125. description={t('app.newApp.agentShortDescription')}
  126. icon={<div className='flex h-6 w-6 items-center justify-center rounded-md bg-components-icon-bg-violet-solid'>
  127. <Logic className='h-4 w-4 text-components-avatar-shape-fill-stop-100' />
  128. </div>}
  129. onClick={() => {
  130. setAppMode('agent-chat')
  131. }} />
  132. <AppTypeCard
  133. active={appMode === 'completion'}
  134. title={t('app.newApp.completeApp')}
  135. description={t('app.newApp.completionShortDescription')}
  136. icon={<div className='flex h-6 w-6 items-center justify-center rounded-md bg-components-icon-bg-teal-solid'>
  137. <ListSparkle className='h-4 w-4 text-components-avatar-shape-fill-stop-100' />
  138. </div>}
  139. onClick={() => {
  140. setAppMode('completion')
  141. }} />
  142. </div>
  143. </div>
  144. <div>
  145. <div className='mb-2'>
  146. <span className='system-2xs-medium-uppercase text-text-tertiary'>{t('app.newApp.forAdvanced')}</span>
  147. </div>
  148. <div className='flex flex-row gap-2'>
  149. <AppTypeCard
  150. active={appMode === 'advanced-chat'}
  151. title={t('app.types.advanced')}
  152. description={t('app.newApp.advancedShortDescription')}
  153. icon={<div className='flex h-6 w-6 items-center justify-center rounded-md bg-components-icon-bg-blue-light-solid'>
  154. <BubbleTextMod className='h-4 w-4 text-components-avatar-shape-fill-stop-100' />
  155. </div>}
  156. onClick={() => {
  157. setAppMode('advanced-chat')
  158. }} />
  159. <AppTypeCard
  160. active={appMode === 'workflow'}
  161. title={t('app.types.workflow')}
  162. description={t('app.newApp.workflowShortDescription')}
  163. icon={<div className='flex h-6 w-6 items-center justify-center rounded-md bg-components-icon-bg-indigo-solid'>
  164. <RiExchange2Fill className='h-4 w-4 text-components-avatar-shape-fill-stop-100' />
  165. </div>}
  166. onClick={() => {
  167. setAppMode('workflow')
  168. }} />
  169. </div>
  170. </div>
  171. <Divider style={{ margin: 0 }} />
  172. <div className='flex items-center space-x-3'>
  173. <div className='flex-1'>
  174. <div className='mb-1 flex h-6 items-center'>
  175. <label className='system-sm-semibold text-text-secondary'>{t('app.newApp.captionName')}</label>
  176. </div>
  177. <Input
  178. value={name}
  179. onChange={e => setName(e.target.value)}
  180. placeholder={t('app.newApp.appNamePlaceholder') || ''}
  181. />
  182. </div>
  183. <AppIcon
  184. iconType={appIcon.type}
  185. icon={appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId}
  186. background={appIcon.type === 'emoji' ? appIcon.background : undefined}
  187. imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
  188. size='xxl' className='cursor-pointer rounded-2xl'
  189. onClick={() => { setShowAppIconPicker(true) }}
  190. />
  191. {showAppIconPicker && <AppIconPicker
  192. onSelect={(payload) => {
  193. setAppIcon(payload)
  194. setShowAppIconPicker(false)
  195. }}
  196. onClose={() => {
  197. setShowAppIconPicker(false)
  198. }}
  199. />}
  200. </div>
  201. <div>
  202. <div className='mb-1 flex h-6 items-center'>
  203. <label className='system-sm-semibold text-text-secondary'>{t('app.newApp.captionDescription')}</label>
  204. <span className='system-xs-regular ml-1 text-text-tertiary'>({t('app.newApp.optional')})</span>
  205. </div>
  206. <Textarea
  207. className='resize-none'
  208. placeholder={t('app.newApp.appDescriptionPlaceholder') || ''}
  209. value={description}
  210. onChange={e => setDescription(e.target.value)}
  211. />
  212. </div>
  213. </div>
  214. {isAppsFull && <AppsFull className='mt-4' loc='app-create' />}
  215. <div className='flex items-center justify-between pb-10 pt-5'>
  216. <div className='system-xs-regular flex cursor-pointer items-center gap-1 text-text-tertiary' onClick={onCreateFromTemplate}>
  217. <span>{t('app.newApp.noIdeaTip')}</span>
  218. <div className='p-[1px]'>
  219. <RiArrowRightLine className='h-3.5 w-3.5' />
  220. </div>
  221. </div>
  222. <div className='flex gap-2'>
  223. <Button onClick={onClose}>{t('app.newApp.Cancel')}</Button>
  224. <Button disabled={isAppsFull || !name} className='gap-1' variant="primary" onClick={handleCreateApp}>
  225. <span>{t('app.newApp.Create')}</span>
  226. <div className='flex gap-0.5'>
  227. <RiCommandLine size={14} className='system-kbd rounded-sm bg-components-kbd-bg-white p-0.5' />
  228. <RiCornerDownLeftLine size={14} className='system-kbd rounded-sm bg-components-kbd-bg-white p-0.5' />
  229. </div>
  230. </Button>
  231. </div>
  232. </div>
  233. </div>
  234. </div>
  235. <div className='relative flex h-full flex-1 shrink justify-start overflow-hidden'>
  236. <div className='absolute left-0 right-0 top-0 h-6 border-b border-b-divider-subtle 2xl:h-[139px]'></div>
  237. <div className='max-w-[760px] border-x border-x-divider-subtle'>
  238. <div className='h-6 2xl:h-[139px]' />
  239. <AppPreview mode={appMode} />
  240. <div className='absolute left-0 right-0 border-b border-b-divider-subtle'></div>
  241. <div className='flex h-[448px] w-[664px] items-center justify-center' style={{ background: 'repeating-linear-gradient(135deg, transparent, transparent 2px, rgba(16,24,40,0.04) 4px,transparent 3px, transparent 6px)' }}>
  242. <AppScreenShot show={appMode === 'chat'} mode='chat' />
  243. <AppScreenShot show={appMode === 'advanced-chat'} mode='advanced-chat' />
  244. <AppScreenShot show={appMode === 'agent-chat'} mode='agent-chat' />
  245. <AppScreenShot show={appMode === 'completion'} mode='completion' />
  246. <AppScreenShot show={appMode === 'workflow'} mode='workflow' />
  247. </div>
  248. <div className='absolute left-0 right-0 border-b border-b-divider-subtle'></div>
  249. </div>
  250. </div>
  251. </div>
  252. </>
  253. }
  254. type CreateAppDialogProps = CreateAppProps & {
  255. show: boolean
  256. }
  257. const CreateAppModal = ({ show, onClose, onSuccess, onCreateFromTemplate }: CreateAppDialogProps) => {
  258. return (
  259. <FullScreenModal
  260. overflowVisible
  261. closable
  262. open={show}
  263. onClose={onClose}
  264. >
  265. <CreateApp onClose={onClose} onSuccess={onSuccess} onCreateFromTemplate={onCreateFromTemplate} />
  266. </FullScreenModal>
  267. )
  268. }
  269. export default CreateAppModal
  270. type AppTypeCardProps = {
  271. icon: React.JSX.Element
  272. title: string
  273. description: string
  274. active: boolean
  275. onClick: () => void
  276. }
  277. function AppTypeCard({ icon, title, description, active, onClick }: AppTypeCardProps) {
  278. return <div
  279. className={
  280. cn(`relative box-content h-[84px] w-[191px] cursor-pointer rounded-xl
  281. border-[0.5px] border-components-option-card-option-border
  282. bg-components-panel-on-panel-item-bg p-3 shadow-xs hover:shadow-md`, active
  283. ? 'shadow-md outline outline-[1.5px] outline-components-option-card-option-selected-border'
  284. : '')
  285. }
  286. onClick={onClick}
  287. >
  288. {icon}
  289. <div className='system-sm-semibold mb-0.5 mt-2 text-text-secondary'>{title}</div>
  290. <div className='system-xs-regular text-text-tertiary'>{description}</div>
  291. </div>
  292. }
  293. function AppPreview({ mode }: { mode: AppMode }) {
  294. const { t } = useTranslation()
  295. const modeToPreviewInfoMap = {
  296. 'chat': {
  297. title: t('app.types.chatbot'),
  298. description: t('app.newApp.chatbotUserDescription'),
  299. link: 'https://docs.dify.ai/guides/application-orchestrate#application_type',
  300. },
  301. 'advanced-chat': {
  302. title: t('app.types.advanced'),
  303. description: t('app.newApp.advancedUserDescription'),
  304. link: 'https://docs.dify.ai/guides/workflow',
  305. },
  306. 'agent-chat': {
  307. title: t('app.types.agent'),
  308. description: t('app.newApp.agentUserDescription'),
  309. link: 'https://docs.dify.ai/guides/application-orchestrate/agent',
  310. },
  311. 'completion': {
  312. title: t('app.newApp.completeApp'),
  313. description: t('app.newApp.completionUserDescription'),
  314. link: null,
  315. },
  316. 'workflow': {
  317. title: t('app.types.workflow'),
  318. description: t('app.newApp.workflowUserDescription'),
  319. link: 'https://docs.dify.ai/guides/workflow',
  320. },
  321. }
  322. const previewInfo = modeToPreviewInfoMap[mode]
  323. return <div className='px-8 py-4'>
  324. <h4 className='system-sm-semibold-uppercase text-text-secondary'>{previewInfo.title}</h4>
  325. <div className='system-xs-regular mt-1 min-h-8 max-w-96 text-text-tertiary'>
  326. <span>{previewInfo.description}</span>
  327. {previewInfo.link && <Link target='_blank' href={previewInfo.link} className='ml-1 text-text-accent'>{t('app.newApp.learnMore')}</Link>}
  328. </div>
  329. </div>
  330. }
  331. function AppScreenShot({ mode, show }: { mode: AppMode; show: boolean }) {
  332. const { theme } = useTheme()
  333. const modeToImageMap = {
  334. 'chat': 'Chatbot',
  335. 'advanced-chat': 'Chatflow',
  336. 'agent-chat': 'Agent',
  337. 'completion': 'TextGenerator',
  338. 'workflow': 'Workflow',
  339. }
  340. return <picture>
  341. <source media="(resolution: 1x)" srcSet={`${basePath}/screenshots/${theme}/${modeToImageMap[mode]}.png`} />
  342. <source media="(resolution: 2x)" srcSet={`${basePath}/screenshots/${theme}/${modeToImageMap[mode]}@2x.png`} />
  343. <source media="(resolution: 3x)" srcSet={`${basePath}/screenshots/${theme}/${modeToImageMap[mode]}@3x.png`} />
  344. <Image className={show ? '' : 'hidden'}
  345. src={`${basePath}/screenshots/${theme}/${modeToImageMap[mode]}.png`}
  346. alt='App Screen Shot'
  347. width={664} height={448} />
  348. </picture>
  349. }