Commit 4edaa95c authored by JzoNg's avatar JzoNg

app menu

parent 067e6b5a
......@@ -31,19 +31,6 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const { appDetail, setAppDetail } = useStore()
const { data: response } = useSWR(detailParams, fetchAppDetail)
const appModeName = (() => {
if (response?.mode === 'chat' || response?.mode === 'advanced-chat')
return t('app.types.chatbot')
if (response?.mode === 'agent-chat')
return t('app.types.agent')
if (response?.mode === 'completion')
return t('app.types.completion')
return t('app.types.workflow')
})()
const navigation = useMemo(() => {
const navs = [
...(isCurrentWorkspaceManager
......@@ -97,7 +84,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
return (
<div className={cn(s.app, 'flex', 'overflow-hidden')}>
<AppSideBar title={response.name} icon={response.icon} icon_background={response.icon_background} desc={appModeName} navigation={navigation} />
<AppSideBar title={response.name} icon={response.icon} icon_background={response.icon_background} desc={response.mode} navigation={navigation} />
<div className="bg-white grow overflow-hidden">
{children}
</div>
......
......@@ -122,6 +122,8 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
message: t('app.newApp.appCreated'),
})
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
mutateApps()
onPlanInfoChanged()
getRedirection(isCurrentWorkspaceManager, newApp, push)
}
catch (e) {
......@@ -175,13 +177,17 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
<span className={s.actionName}>{t('common.operation.settings')}</span>
</button>
<Divider className="!my-1" />
<button className={s.actionItem} onClick={onClickDuplicate} disabled={detailState.loading}>
<span className={s.actionName}>{t('app.duplicate')}</span>
</button>
<button className={s.actionItem} onClick={onClickExport} disabled={detailState.loading}>
<span className={s.actionName}>{t('app.export')}</span>
</button>
<Divider className="!my-1" />
{app.mode !== 'completion' && (
<>
<button className={s.actionItem} onClick={onClickDuplicate} disabled={detailState.loading}>
<span className={s.actionName}>{t('app.duplicate')}</span>
</button>
<button className={s.actionItem} onClick={onClickExport} disabled={detailState.loading}>
<span className={s.actionName}>{t('app.export')}</span>
</button>
<Divider className="!my-1" />
</>
)}
<div
className={cn(s.actionItem, s.deleteActionItem, 'group')}
onClick={onClickDelete}
......
......@@ -49,11 +49,10 @@ const Apps = () => {
)
const anchorRef = useRef<HTMLDivElement>(null)
// #TODO# query key ???
const options = [
{ value: 'all', text: t('app.types.all') },
{ value: 'chat', text: t('app.types.chatbot') },
{ value: 'agent-chat', text: t('app.types.agent') },
{ value: 'agent', text: t('app.types.agent') },
{ value: 'workflow', text: t('app.types.workflow') },
]
......
import { useTranslation } from 'react-i18next'
import { useRouter } from 'next/navigation'
import { useContext, useContextSelector } from 'use-context-selector'
import cn from 'classnames'
import React, { useCallback, useState } from 'react'
import AppIcon from '../base/app-icon'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
import Divider from '@/app/components/base/divider'
import Confirm from '@/app/components/base/confirm'
import { useStore as useAppStore } from '@/app/components/app/store'
import { ToastContext } from '@/app/components/base/toast'
import AppsContext from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps'
import DuplicateAppModal from '@/app/components/app/duplicate-modal'
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
import CreateAppModal from '@/app/components/explore/create-app-modal'
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { getRedirection } from '@/utils/app-redirection'
export type IAppInfoProps = {
expand: boolean
}
const AppInfo = ({ expand }: 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 [open, setOpen] = useState(false)
const [showEditModal, setShowEditModal] = useState(false)
const [showDuplicateModal, setShowDuplicateModal] = useState(false)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const mutateApps = useContextSelector(
AppsContext,
state => state.mutateApps,
)
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
name,
icon,
icon_background,
description,
}) => {
if (!appDetail)
return
try {
const app = await updateAppInfo({
appID: appDetail.id,
name,
icon,
icon_background,
description,
})
setShowEditModal(false)
notify({
type: 'success',
message: t('app.editDone'),
})
console.log(app.description)
setAppDetail(app)
mutateApps()
}
catch (e) {
notify({ type: 'error', message: t('app.editFailed') })
}
}, [appDetail, mutateApps, notify, setAppDetail, t])
const onCopy: DuplicateAppModalProps['onConfirm'] = async ({ name, icon, icon_background }) => {
if (!appDetail)
return
try {
const newApp = await copyApp({
appID: appDetail.id,
name,
icon,
icon_background,
mode: appDetail.mode,
})
setShowDuplicateModal(false)
notify({
type: 'success',
message: t('app.newApp.appCreated'),
})
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
mutateApps()
onPlanInfoChanged()
getRedirection(true, newApp, replace)
}
catch (e) {
notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
}
}
const onExport = async () => {
if (!appDetail)
return
try {
const { data } = await exportAppConfig(appDetail.id)
const a = document.createElement('a')
const file = new Blob([data], { type: 'application/yaml' })
a.href = URL.createObjectURL(file)
a.download = `${appDetail.name}.yml`
a.click()
}
catch (e) {
notify({ type: 'error', message: t('app.exportFailed') })
}
}
const onConfirmDelete = useCallback(async () => {
if (!appDetail)
return
try {
await deleteApp(appDetail.id)
notify({ type: 'success', message: t('app.appDeleted') })
mutateApps()
onPlanInfoChanged()
replace('/apps')
}
catch (e: any) {
notify({
type: 'error',
message: `${t('app.appDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}`,
})
}
setShowConfirmDelete(false)
}, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, t])
if (!appDetail)
return null
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
offset={4}
>
<div className='relative'>
<PortalToFollowElemTrigger
onClick={() => setOpen(v => !v)}
className='block'
>
<div className='flex cursor-pointer'>
<div className='shrink-0 mr-2'>
<AppIcon icon={appDetail.icon} background={appDetail.icon_background} />
</div>
{expand && (
<div className="grow w-0 pt-[2px]">
<div className='flex justify-between items-center text-sm leading-4 font-medium text-gray-900'>
<div className='truncate' title={appDetail.name}>{appDetail.name}</div>
<ChevronDown className='shrink-0 ml-[2px] w-3 h-3 text-gray-500' />
</div>
<div className='flex items-center text-xs leading-[18px] font-medium text-gray-500 gap-1'>
{appDetail.mode === 'advanced-chat' && (
<>
<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>
<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>
</>
)}
{appDetail.mode === 'agent-chat' && (
<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>
)}
{appDetail.mode === 'chat' && (
<>
<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>
<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>
</>
)}
{appDetail.mode === 'completion' && (
<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>
)}
{appDetail.mode === 'workflow' && (
<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>
)}
</div>
</div>
)}
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[1002]'>
<div className='w-[320px] bg-white rounded-2xl shadow-xl'>
{/* header */}
<div className={cn('flex pl-4 pt-3 pr-3', !appDetail.description && 'pb-2')}>
<div className='shrink-0 mr-2'>
<AppIcon icon={appDetail.icon} background={appDetail.icon_background} />
</div>
<div className='grow w-0 pt-[2px]'>
<div title={appDetail.name} className='flex justify-between items-center text-sm leading-4 font-medium text-gray-900 truncate'>{appDetail.name}</div>
<div className='flex items-center text-xs leading-[18px] font-medium text-gray-500 gap-1'>
{appDetail.mode === 'advanced-chat' && (
<>
<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>
<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>
</>
)}
{appDetail.mode === 'agent-chat' && (
<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>
)}
{appDetail.mode === 'chat' && (
<>
<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>
<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>
</>
)}
{appDetail.mode === 'completion' && (
<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>
)}
{appDetail.mode === 'workflow' && (
<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>
)}
</div>
</div>
</div>
{/* desscription */}
{appDetail.description && (
<div className='px-4 py-2 text-gray-500 text-xs leading-[18px]'>{appDetail.description}</div>
)}
{/* operations */}
<div></div>
<Divider className="!my-1" />
<div className="w-full py-1">
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={() => {
setOpen(false)
setShowEditModal(true)
}}>
<span className='text-gray-700 text-sm leading-5'>{t('app.editApp')}</span>
</div>
{appDetail.mode !== 'completion' && (
<>
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={() => {
setOpen(false)
setShowDuplicateModal(true)
}}>
<span className='text-gray-700 text-sm leading-5'>{t('app.duplicate')}</span>
</div>
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={onExport}>
<span className='text-gray-700 text-sm leading-5'>{t('app.export')}</span>
</div>
</>
)}
<Divider className="!my-1" />
<div className='group h-9 py-2 px-3 mx-1 flex items-center hover:bg-red-50 rounded-lg cursor-pointer' onClick={() => {
setOpen(false)
setShowConfirmDelete(true)
}}>
<span className='text-gray-700 text-sm leading-5 group-hover:text-red-500'>
{t('common.operation.delete')}
</span>
</div>
</div>
</div>
</PortalToFollowElemContent>
{showEditModal && (
<CreateAppModal
isEditModal
appIcon={appDetail.icon}
appIconBackground={appDetail.icon_background}
appName={appDetail.name}
appDescription={appDetail.description}
show={showEditModal}
onConfirm={onEdit}
onHide={() => setShowEditModal(false)}
/>
)}
{showDuplicateModal && (
<DuplicateAppModal
appName={appDetail.name}
icon={appDetail.icon}
icon_background={appDetail.icon_background}
show={showDuplicateModal}
onConfirm={onCopy}
onHide={() => setShowDuplicateModal(false)}
/>
)}
{showConfirmDelete && (
<Confirm
title={t('app.deleteAppConfirmTitle')}
content={t('app.deleteAppConfirmContent')}
isShow={showConfirmDelete}
onClose={() => setShowConfirmDelete(false)}
onConfirm={onConfirmDelete}
onCancel={() => setShowConfirmDelete(false)}
/>
)}
</div>
</PortalToFollowElem>
)
}
export default React.memo(AppInfo)
......@@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react'
import NavLink from './navLink'
import type { NavIcon } from './navLink'
import AppBasic from './basic'
import AppInfo from './app-info'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import {
AlignLeft01,
......@@ -62,14 +63,19 @@ const AppDetailNav = ({ title, desc, icon, icon_background, navigation, extraInf
${expand ? 'p-4' : 'p-2'}
`}
>
<AppBasic
mode={modeState}
iconType={iconType}
icon={icon}
icon_background={icon_background}
name={title}
type={desc}
/>
{iconType === 'app' && (
<AppInfo expand={expand}/>
)}
{iconType !== 'app' && (
<AppBasic
mode={modeState}
iconType={iconType}
icon={icon}
icon_background={icon_background}
name={title}
type={desc}
/>
)}
</div>
<nav
className={`
......
......@@ -149,7 +149,10 @@ const Apps = ({
</div>
{isShowCreateModal && (
<CreateAppModal
appIcon={currApp?.app.icon || ''}
appIconBackground={currApp?.app.icon_background || ''}
appName={currApp?.app.name || ''}
appDescription={currApp?.app.description || ''}
show={isShowCreateModal}
onConfirm={onCreate}
onHide={() => setIsShowCreateModal(false)}
......
......@@ -11,9 +11,12 @@ import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
export type CreateAppModalProps = {
appName: string
appDescription?: string
show: boolean
isEditModal?: boolean
appName: string
appDescription: string
appIcon: string
appIconBackground: string
onConfirm: (info: {
name: string
icon: string
......@@ -24,9 +27,12 @@ export type CreateAppModalProps = {
}
const CreateAppModal = ({
show = false,
isEditModal = false,
appIcon,
appIconBackground,
appName,
appDescription,
show = false,
onConfirm,
onHide,
}: CreateAppModalProps) => {
......@@ -34,7 +40,7 @@ const CreateAppModal = ({
const [name, setName] = React.useState(appName)
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const [emoji, setEmoji] = useState({ icon: '🤖', icon_background: '#FFEAD5' })
const [emoji, setEmoji] = useState({ icon: appIcon, icon_background: appIconBackground })
const [description, setDescription] = useState(appDescription || '')
const { plan, enableBilling } = useProviderContext()
......@@ -64,7 +70,12 @@ const CreateAppModal = ({
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onHide}>
<XClose className='w-4 h-4 text-gray-500' />
</div>
<div className='mb-9 font-semibold text-xl leading-[30px] text-gray-900'>{t('explore.appCustomize.title', { name: appName })}</div>
{isEditModal && (
<div className='mb-9 font-semibold text-xl leading-[30px] text-gray-900'>{t('app.editAppTitle')}</div>
)}
{!isEditModal && (
<div className='mb-9 font-semibold text-xl leading-[30px] text-gray-900'>{t('explore.appCustomize.title', { name: appName })}</div>
)}
<div className='mb-9'>
{/* icon & name */}
<div className='pt-2'>
......@@ -78,16 +89,6 @@ const CreateAppModal = ({
className='grow h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs'
/>
</div>
{showEmojiPicker && <EmojiPicker
onSelect={(icon, icon_background) => {
setEmoji({ icon, icon_background })
setShowEmojiPicker(false)
}}
onClose={() => {
setEmoji({ icon: '🤖', icon_background: '#FFEAD5' })
setShowEmojiPicker(false)
}}
/>}
</div>
{/* description */}
<div className='pt-2'>
......@@ -99,10 +100,10 @@ const CreateAppModal = ({
onChange={e => setDescription(e.target.value)}
/>
</div>
{isAppsFull && <AppsFull loc='app-explore-create' />}
{!isEditModal && isAppsFull && <AppsFull loc='app-explore-create' />}
</div>
<div className='flex flex-row-reverse'>
<Button disabled={isAppsFull} className='w-24 ml-2' type='primary' onClick={submit}>{t('common.operation.create')}</Button>
<Button disabled={!isEditModal && isAppsFull} className='w-24 ml-2' type='primary' onClick={submit}>{!isEditModal ? t('common.operation.create') : t('common.operation.save')}</Button>
<Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button>
</div>
</Modal>
......@@ -112,7 +113,7 @@ const CreateAppModal = ({
setShowEmojiPicker(false)
}}
onClose={() => {
setEmoji({ icon: '🤖', icon_background: '#FFEAD5' })
setEmoji({ icon: appIcon, icon_background: appIconBackground })
setShowEmojiPicker(false)
}}
/>}
......
......@@ -58,9 +58,10 @@ const translation = {
appCreated: 'App created',
appCreateFailed: 'Failed to create app',
},
editApp: {
startToEdit: 'Edit App',
},
editApp: 'Edit Info',
editAppTitle: 'Edit App Info',
editDone: 'App info updated',
editFailed: 'Failed to update app info',
emoji: {
ok: 'OK',
cancel: 'Cancel',
......
......@@ -57,9 +57,10 @@ const translation = {
appCreated: '应用已创建',
appCreateFailed: '应用创建失败',
},
editApp: {
startToEdit: '编辑应用',
},
editApp: '编辑信息',
editAppTitle: '编辑应用信息',
editDone: '应用信息已更新',
editFailed: '更新应用信息失败',
emoji: {
ok: '确认',
cancel: '取消',
......
import type { AppMode } from '@/types/app'
export type AppBasicInfo = {
id: string
name: string
mode: AppMode
icon: string
icon_background: string
is_agent: boolean
name: string
description: string
}
export type AppCategory = 'Writing' | 'Translate' | 'HR' | 'Programming' | 'Assistant'
......
import type { Fetcher } from 'swr'
import { del, get, post } from './base'
import { del, get, post, put } from './base'
import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, GenerationIntroductionResponse, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse } from '@/models/app'
import type { CommonResponse } from '@/models/common'
import type { AppMode, ModelConfig } from '@/types/app'
......@@ -20,6 +20,10 @@ export const createApp: Fetcher<AppDetailResponse, { name: string; icon: string;
return post<AppDetailResponse>('apps', { body: { name, icon, icon_background, mode, description, model_config: config } })
}
export const updateAppInfo: Fetcher<AppDetailResponse, { appID: string; name: string; icon: string; icon_background: string; description: string }> = ({ appID, name, icon, icon_background, description }) => {
return put<AppDetailResponse>(`apps/${appID}`, { body: { name, icon, icon_background, description } })
}
export const copyApp: Fetcher<AppDetailResponse, { appID: string; name: string; icon: string; icon_background: string; mode: AppMode; description?: string }> = ({ appID, name, icon, icon_background, mode, description }) => {
return post<AppDetailResponse>(`apps/${appID}/copy`, { body: { name, icon, icon_background, mode, description } })
}
......
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