Commit 804a0904 authored by JzoNg's avatar JzoNg

app templates

parent 14cfb310
'use client'
import type { MouseEventHandler } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import useSWR from 'swr'
import classNames from 'classnames'
import { useRouter } from 'next/navigation'
import { useContext, useContextSelector } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import style from '../list.module.css'
import AppModeLabel from './AppModeLabel'
import Button from '@/app/components/base/button'
import Dialog from '@/app/components/base/dialog'
import type { AppMode } from '@/types/app'
import { ToastContext } from '@/app/components/base/toast'
import { createApp, fetchAppTemplates } from '@/service/apps'
import AppIcon from '@/app/components/base/app-icon'
import AppsContext, { useAppContext } from '@/context/app-context'
import EmojiPicker from '@/app/components/base/emoji-picker'
import { useProviderContext } from '@/context/provider-context'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { AiText } from '@/app/components/base/icons/src/vender/solid/communication'
type NewAppDialogProps = {
show: boolean
onSuccess?: () => void
onClose?: () => void
}
const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
const router = useRouter()
const { notify } = useContext(ToastContext)
const { isCurrentWorkspaceManager } = useAppContext()
const { t } = useTranslation()
const nameInputRef = useRef<HTMLInputElement>(null)
const [newAppMode, setNewAppMode] = useState<AppMode>()
const [isWithTemplate, setIsWithTemplate] = useState(false)
const [selectedTemplateIndex, setSelectedTemplateIndex] = useState<number>(-1)
// Emoji Picker
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' })
const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
const { data: templates, mutate } = useSWR({ url: '/app-templates' }, fetchAppTemplates)
const mutateTemplates = useCallback(
() => mutate(),
[],
)
useEffect(() => {
if (show) {
mutateTemplates()
setIsWithTemplate(false)
}
}, [mutateTemplates, show])
const { plan, enableBilling } = useProviderContext()
const isAppsFull = (enableBilling && plan.usage.buildApps >= plan.total.buildApps)
const isCreatingRef = useRef(false)
const onCreate: MouseEventHandler = useCallback(async () => {
const name = nameInputRef.current?.value
if (!name) {
notify({ type: 'error', message: t('app.newApp.nameNotEmpty') })
return
}
if (!templates || (isWithTemplate && !(selectedTemplateIndex > -1))) {
notify({ type: 'error', message: t('app.newApp.appTemplateNotSelected') })
return
}
if (!isWithTemplate && !newAppMode) {
notify({ type: 'error', message: t('app.newApp.appTypeRequired') })
return
}
if (isCreatingRef.current)
return
isCreatingRef.current = true
try {
const app = await createApp({
name,
icon: emoji.icon,
icon_background: emoji.icon_background,
mode: isWithTemplate ? templates.data[selectedTemplateIndex].mode : newAppMode!,
config: isWithTemplate ? templates.data[selectedTemplateIndex].model_config : undefined,
})
if (onSuccess)
onSuccess()
if (onClose)
onClose()
notify({ type: 'success', message: t('app.newApp.appCreated') })
mutateApps()
router.push(`/app/${app.id}/${isCurrentWorkspaceManager ? 'configuration' : 'overview'}`)
}
catch (e) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
isCreatingRef.current = false
}, [isWithTemplate, newAppMode, notify, router, templates, selectedTemplateIndex, emoji])
return <>
{showEmojiPicker && <EmojiPicker
onSelect={(icon, icon_background) => {
setEmoji({ icon, icon_background })
setShowEmojiPicker(false)
}}
onClose={() => {
setEmoji({ icon: '🤖', icon_background: '#FFEAD5' })
setShowEmojiPicker(false)
}}
/>}
<Dialog
show={show}
title={t('app.newApp.startToCreate')}
footer={
<>
<Button onClick={onClose}>{t('app.newApp.Cancel')}</Button>
<Button disabled={isAppsFull} type="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
</>
}
>
<div className='overflow-y-auto'>
<div className={style.newItemCaption}>
<h3 className='inline'>{t('app.newApp.captionAppType')}</h3>
{isWithTemplate && (
<>
<span className='block ml-[9px] mr-[9px] w-[1px] h-[13px] bg-gray-200' />
<span
className='inline-flex items-center gap-1 text-xs font-medium cursor-pointer text-primary-600'
onClick={() => setIsWithTemplate(false)}
>
{t('app.newApp.hideTemplates')}
</span>
</>
)}
</div>
{!isWithTemplate && (
(
<>
<ul className='grid grid-cols-1 md:grid-cols-2 gap-4'>
<li
className={classNames(style.listItem, style.selectable, newAppMode === 'chat' && style.selected)}
onClick={() => setNewAppMode('chat')}
>
<div className={style.listItemTitle}>
<span className={style.newItemIcon}>
<span className={classNames(style.newItemIconImage, style.newItemIconChat)} />
</span>
<div className={style.listItemHeading}>
<div className={style.listItemHeadingContent}>{t('app.newApp.chatApp')}</div>
</div>
<div className='flex items-center h-[18px] border border-indigo-300 px-1 rounded-[5px] text-xs font-medium text-indigo-600 uppercase truncate'>{t('app.newApp.agentAssistant')}</div>
</div>
<div className={`${style.listItemDescription} ${style.noClip}`}>{t('app.newApp.chatAppIntro')}</div>
{/* <div className={classNames(style.listItemFooter, 'justify-end')}>
<a className={style.listItemLink} href='https://udify.app/chat/7CQBa5yyvYLSkZtx' target='_blank' rel='noopener noreferrer'>{t('app.newApp.previewDemo')}<span className={classNames(style.linkIcon, style.grayLinkIcon)} /></a>
</div> */}
</li>
<li
className={classNames(style.listItem, style.selectable, newAppMode === 'completion' && style.selected)}
onClick={() => setNewAppMode('completion')}
>
<div className={style.listItemTitle}>
<span className={style.newItemIcon}>
{/* <span className={classNames(style.newItemIconImage, style.newItemIconComplete)} /> */}
<AiText className={classNames('w-5 h-5', newAppMode === 'completion' ? 'text-[#155EEF]' : 'text-gray-700')} />
</span>
<div className={style.listItemHeading}>
<div className={style.listItemHeadingContent}>{t('app.newApp.completeApp')}</div>
</div>
</div>
<div className={`${style.listItemDescription} ${style.noClip}`}>{t('app.newApp.completeAppIntro')}</div>
</li>
</ul>
</>
)
)}
{isWithTemplate && (
<ul className='grid grid-cols-1 md:grid-cols-2 gap-4'>
{templates?.data?.map((template, index) => (
<li
key={index}
className={classNames(style.listItem, style.selectable, selectedTemplateIndex === index && style.selected)}
onClick={() => setSelectedTemplateIndex(index)}
>
<div className={style.listItemTitle}>
<AppIcon size='small' />
<div className={style.listItemHeading}>
<div className={style.listItemHeadingContent}>{template.name}</div>
</div>
</div>
<div className={style.listItemDescription}>{template.model_config?.pre_prompt}</div>
<div className='inline-block pl-3.5'>
<AppModeLabel mode={template.mode} isAgent={template.model_config.agent_mode.enabled} className='mt-2' />
</div>
</li>
))}
</ul>
)}
<div className='mt-8'>
<h3 className={style.newItemCaption}>{t('app.newApp.captionName')}</h3>
<div className='flex items-center justify-between gap-3'>
<AppIcon size='large' onClick={() => { setShowEmojiPicker(true) }} className='cursor-pointer' icon={emoji.icon} background={emoji.icon_background} />
<input ref={nameInputRef} className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' placeholder={t('app.appNamePlaceholder') || ''} />
</div>
</div>
{
!isWithTemplate && (
<div className='flex items-center h-[34px] mt-2'>
<span
className='inline-flex items-center gap-1 text-xs font-medium cursor-pointer text-primary-600'
onClick={() => setIsWithTemplate(true)}
>
{t('app.newApp.showTemplates')}<span className={style.rightIcon} />
</span>
</div>
)
}
</div>
{isAppsFull && <AppsFull loc='app-create' />}
</Dialog>
</>
}
export default NewAppDialog
......@@ -74,7 +74,7 @@ const AppForm = ({
}, [name, notify, t, appMode, emoji.icon, emoji.icon_background, description, onConfirm, onHide, mutateApps, router, isCurrentWorkspaceManager])
return (
<>
<div className='overflow-y-auto'>
{/* app type */}
<div className='pt-2 px-8'>
<div className='py-2 text-sm leading-[20px] font-medium text-gray-900'>{t('app.newApp.captionAppType')}</div>
......@@ -162,7 +162,7 @@ const AppForm = ({
<Button className='mr-2 text-gray-700 text-sm font-medium' onClick={onHide}>{t('app.newApp.Cancel')}</Button>
<Button className='text-sm font-medium' disabled={isAppsFull || !name} type="primary" onClick={onCreate}>{t('app.newApp.Create')}</Button>
</div>
</>
</div>
)
}
......
......@@ -3,6 +3,7 @@
import { useTranslation } from 'react-i18next'
import NewAppDialog from './newAppDialog'
import AppForm from './appForm'
import AppList, { PageType } from '@/app/components/explore/app-list'
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
type CreateAppDialogProps = {
......@@ -21,18 +22,19 @@ const CreateAppDialog = ({ show, onSuccess, onClose }: CreateAppDialogProps) =>
onClose={() => {}}
>
{/* blank form */}
<div className='shrink-0 max-w-[480px] h-full bg-white overflow-y-auto'>
<div className='shrink-0 flex flex-col max-w-[480px] h-full bg-white'>
{/* Heading */}
<div className='sticky top-0 pl-8 pr-6 pt-6 pb-3 rounded-ss-xl text-xl leading-[30px] font-semibold text-gray-900'>{t('app.newApp.startFromBlank')}</div>
<div className='shrink-0 pl-8 pr-6 pt-6 pb-3 bg-white rounded-ss-xl text-xl leading-[30px] font-semibold text-gray-900 z-10'>{t('app.newApp.startFromBlank')}</div>
{/* app form */}
<AppForm onHide={onClose} onConfirm={onSuccess}/>
</div>
{/* template list */}
<div className='grow bg-gray-100'>
<div className='sticky top-0 pl-8 pr-6 pt-6 pb-3 rounded-se-xl text-xl leading-[30px] font-semibold text-gray-900'>{t('app.newApp.startFromTemplate')}</div>
<div className='grow flex flex-col h-full bg-gray-100'>
<div className='shrink-0 pl-8 pr-6 pt-6 pb-3 bg-gray-100 rounded-se-xl text-xl leading-[30px] font-semibold text-gray-900 z-10'>{t('app.newApp.startFromTemplate')}</div>
<AppList pageType={PageType.CREATE} />
</div>
<div className='absolute top-6 left-[464px] w-8 h-8 p-1 bg-white border-2 border-gray-200 rounded-2xl text-xs leading-[20px] font-medium text-gray-500 cursor-default'>OR</div>
<div className='absolute right-6 top-6 p-2 cursor-pointer' onClick={onClose}>
<div className='absolute top-6 left-[464px] w-8 h-8 p-1 bg-white border-2 border-gray-200 rounded-2xl text-xs leading-[20px] font-medium text-gray-500 cursor-default z-20'>OR</div>
<div className='absolute right-6 top-6 p-2 cursor-pointer z-20' onClick={onClose}>
<XClose className='w-4 h-4 text-gray-500' />
</div>
</NewAppDialog>
......
......@@ -12,15 +12,17 @@ export type AppCardProps = {
app: App
canCreate: boolean
onCreate: () => void
isExplore: boolean
}
const AppCard = ({
app,
canCreate,
onCreate,
isExplore,
}: AppCardProps) => {
const { t } = useTranslation()
const { app: appBasicInfo, is_agent } = app
const { app: appBasicInfo } = app
return (
<div className={cn(s.wrap, 'col-span-1 bg-white border-2 border-solid border-transparent rounded-lg shadow-sm min-h-[160px] flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg')}>
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
......@@ -32,18 +34,24 @@ const AppCard = ({
<div className='mb-3 px-[14px] h-9 text-xs leading-normal text-gray-500 line-clamp-2'>{app.description}</div>
<div className='flex items-center flex-wrap min-h-[42px] px-[14px] pt-2 pb-[10px]'>
<div className={s.mode}>
<AppModeLabel mode={appBasicInfo.mode} isAgent={is_agent} />
<AppModeLabel mode={appBasicInfo.mode} />
</div>
{
canCreate && (
<div className={cn(s.opWrap, 'flex items-center w-full space-x-2')}>
<Button type='primary' className='grow flex items-center !h-7' onClick={() => onCreate()}>
<PlusIcon className='w-4 h-4 mr-1' />
<span className='text-xs'>{t('explore.appCard.addToWorkspace')}</span>
</Button>
</div>
)
}
{isExplore && canCreate && (
<div className={cn(s.opWrap, 'flex items-center w-full space-x-2')}>
<Button type='primary' className='grow flex items-center !h-7' onClick={() => onCreate()}>
<PlusIcon className='w-4 h-4 mr-1' />
<span className='text-xs'>{t('explore.appCard.addToWorkspace')}</span>
</Button>
</div>
)}
{!isExplore && (
<div className={cn(s.opWrap, 'flex items-center w-full space-x-2')}>
<Button type='primary' className='grow flex items-center !h-7' onClick={() => onCreate()}>
<PlusIcon className='w-4 h-4 mr-1' />
<span className='text-xs'>{t('app.newApp.useTemplate')}</span>
</Button>
</div>
)}
</div>
</div>
)
......
'use client'
import type { FC } from 'react'
import React, { useEffect } from 'react'
import cn from 'classnames'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
......@@ -19,7 +20,18 @@ import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { type AppMode } from '@/types/app'
import { useAppContext } from '@/context/app-context'
const Apps: FC = () => {
type AppsProps = {
pageType?: PageType
}
export enum PageType {
EXPLORE = 'explore',
CREATE = 'create',
}
const Apps = ({
pageType = PageType.EXPLORE,
}: AppsProps) => {
const { t } = useTranslation()
const { isCurrentWorkspaceManager } = useAppContext()
const router = useRouter()
......@@ -81,23 +93,36 @@ const Apps: FC = () => {
}
return (
<div className='h-full flex flex-col border-l border-gray-200'>
<div className='shrink-0 pt-6 px-12'>
<div className={`mb-1 ${s.textGradient} text-xl font-semibold`}>{t('explore.apps.title')}</div>
<div className='text-gray-500 text-sm'>{t('explore.apps.description')}</div>
</div>
<div className={cn(
'flex flex-col border-l border-gray-200',
pageType === PageType.EXPLORE ? 'h-full' : 'h-[calc(100%-76px)]',
)}>
{pageType === PageType.EXPLORE && (
<div className='shrink-0 pt-6 px-12'>
<div className={`mb-1 ${s.textGradient} text-xl font-semibold`}>{t('explore.apps.title')}</div>
<div className='text-gray-500 text-sm'>{t('explore.apps.description')}</div>
</div>
)}
<Category
className='mt-6 px-12'
className={cn(pageType === PageType.EXPLORE ? 'mt-6 px-12' : 'px-8 py-2')}
list={categories}
value={currCategory}
onChange={setCurrCategory}
/>
<div className='relative flex flex-1 mt-6 pb-6 flex-col overflow-auto bg-gray-100 shrink-0 grow'>
<div className={cn(
'relative flex flex-1 pb-6 flex-col overflow-auto bg-gray-100 shrink-0 grow',
pageType === PageType.EXPLORE ? 'mt-6' : 'mt-0 pt-2',
)}>
<nav
className={`${s.appList} grid content-start gap-4 px-6 sm:px-12 shrink-0`}>
className={cn(
s.appList,
'grid content-start shrink-0',
pageType === PageType.EXPLORE ? 'gap-4 px-6 sm:px-12' : 'gap-3 px-8',
)}>
{currList.map(app => (
<AppCard
key={app.app_id}
isExplore={pageType === PageType.EXPLORE}
app={app}
canCreate={hasEditPermission}
onCreate={() => {
......@@ -108,7 +133,6 @@ const Apps: FC = () => {
))}
</nav>
</div>
{isShowCreateModal && (
<CreateAppModal
appName={currApp?.app.name || ''}
......
......@@ -23,13 +23,15 @@ const Category: FC<ICategoryProps> = ({
}) => {
const { t } = useTranslation()
const itemClassName = (isSelected: boolean) => cn(isSelected ? 'bg-white text-primary-600 border-gray-200 font-semibold' : 'border-transparent font-medium', 'flex items-center h-7 px-3 border cursor-pointer rounded-lg')
const itemStyle = (isSelected: boolean) => isSelected ? { boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)' } : {}
const itemClassName = (isSelected: boolean) => cn(
'px-3 py-[5px] h-[28px] rounded-lg border-[0.5px] border-transparent text-gray-700 font-medium leading-[18px] cursor-pointer hover:bg-gray-200',
isSelected && 'bg-white border-gray-200 shadow-xs text-primary-600 hover:bg-white',
)
return (
<div className={cn(className, 'flex space-x-1 text-[13px] flex-wrap')}>
<div
className={itemClassName(value === '')}
style={itemStyle(value === '')}
onClick={() => onChange('')}
>
{t('explore.apps.allCategories')}
......@@ -38,7 +40,6 @@ const Category: FC<ICategoryProps> = ({
<div
key={name}
className={itemClassName(name === value)}
style={itemStyle(name === value)}
onClick={() => onChange(name)}
>
{categoryI18n[name] ? t(`explore.category.${name}`) : name}
......
......@@ -54,6 +54,7 @@ const CreateAppModal = ({
<Modal
isShow={show}
onClose={() => { }}
wrapperClassName='z-40'
className={cn(s.modal, '!max-w-[480px]', 'px-8')}
>
<span className={s.close} onClick={onHide} />
......
......@@ -9,7 +9,7 @@ import { flatten } from 'lodash-es'
import Nav from '../nav'
import { Robot, RobotActive } from '../../base/icons/src/public/header-nav/studio'
import { fetchAppDetail, fetchAppList } from '@/service/apps'
import NewAppDialog from '@/app/(commonLayout)/apps/NewAppDialog'
import CreateAppDialog from '@/app/components/app/create-app-dialog'
import type { AppListResponse } from '@/models/app'
import { useAppContext } from '@/context/app-context'
......@@ -54,7 +54,11 @@ const AppNav = () => {
onCreate={() => setShowNewAppDialog(true)}
onLoadmore={handleLoadmore}
/>
<NewAppDialog show={showNewAppDialog} onClose={() => setShowNewAppDialog(false)} />
<CreateAppDialog
show={showNewAppDialog}
onClose={() => setShowNewAppDialog(false)}
onSuccess={() => {}}
/>
</>
)
}
......
......@@ -6,7 +6,7 @@ import { Menu, Transition } from '@headlessui/react'
import { useRouter } from 'next/navigation'
import Indicator from '../indicator'
import type { AppDetailResponse } from '@/models/app'
import NewAppDialog from '@/app/(commonLayout)/apps/NewAppDialog'
import CreateAppDialog from '@/app/components/app/create-app-dialog'
import AppIcon from '@/app/components/base/app-icon'
import { useAppContext } from '@/context/app-context'
......@@ -101,7 +101,11 @@ export default function AppSelector({ appItems, curApp }: IAppSelectorProps) {
</Menu.Items>
</Transition>
</Menu>
<NewAppDialog show={showNewAppDialog} onClose={() => setShowNewAppDialog(false)} />
<CreateAppDialog
show={showNewAppDialog}
onClose={() => setShowNewAppDialog(false)}
onSuccess={() => {}}
/>
</div>
)
}
......@@ -30,6 +30,7 @@ const translation = {
appNamePlaceholder: 'Give your app a name',
captionDescription: 'Description',
appDescriptionPlaceholder: 'Enter the description of the app',
useTemplate: 'Use this template',
previewDemo: 'Preview demo',
chatApp: 'Assistant',
chatAppIntro:
......
......@@ -30,6 +30,7 @@ const translation = {
appNamePlaceholder: '给你的应用起个名字',
captionDescription: '描述',
appDescriptionPlaceholder: '输入应用的描述',
useTemplate: '使用该模板',
previewDemo: '预览 Demo',
chatApp: '助手',
chatAppIntro:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment