diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx index 09e511b9a6..6e24d0e30f 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout.tsx @@ -31,19 +31,6 @@ const AppDetailLayout: FC = (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 = (props) => { return (
- +
{children}
diff --git a/web/app/(commonLayout)/apps/AppCard.tsx b/web/app/(commonLayout)/apps/AppCard.tsx index 8764a722b2..53e24f0b89 100644 --- a/web/app/(commonLayout)/apps/AppCard.tsx +++ b/web/app/(commonLayout)/apps/AppCard.tsx @@ -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) => { {t('common.operation.settings')} - - - + {app.mode !== 'completion' && ( + <> + + + + + )}
{ ) const anchorRef = useRef(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') }, ] diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx new file mode 100644 index 0000000000..ccfb22c781 --- /dev/null +++ b/web/app/components/app-sidebar/app-info.tsx @@ -0,0 +1,301 @@ +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 ( + +
+ setOpen(v => !v)} + className='block' + > +
+
+ +
+ {expand && ( +
+
+
{appDetail.name}
+ +
+
+ {appDetail.mode === 'advanced-chat' && ( + <> +
{t('app.types.chatbot').toUpperCase()}
+
{t('app.newApp.advanced').toUpperCase()}
+ + )} + {appDetail.mode === 'agent-chat' && ( +
{t('app.types.agent').toUpperCase()}
+ )} + {appDetail.mode === 'chat' && ( + <> +
{t('app.types.chatbot').toUpperCase()}
+
{(t('app.newApp.basic').toUpperCase())}
+ + )} + {appDetail.mode === 'completion' && ( +
{t('app.types.completion').toUpperCase()}
+ )} + {appDetail.mode === 'workflow' && ( +
{t('app.types.workflow').toUpperCase()}
+ )} +
+
+ )} +
+
+ +
+ {/* header */} +
+
+ +
+
+
{appDetail.name}
+
+ {appDetail.mode === 'advanced-chat' && ( + <> +
{t('app.types.chatbot').toUpperCase()}
+
{t('app.newApp.advanced').toUpperCase()}
+ + )} + {appDetail.mode === 'agent-chat' && ( +
{t('app.types.agent').toUpperCase()}
+ )} + {appDetail.mode === 'chat' && ( + <> +
{t('app.types.chatbot').toUpperCase()}
+
{(t('app.newApp.basic').toUpperCase())}
+ + )} + {appDetail.mode === 'completion' && ( +
{t('app.types.completion').toUpperCase()}
+ )} + {appDetail.mode === 'workflow' && ( +
{t('app.types.workflow').toUpperCase()}
+ )} +
+
+
+ {/* desscription */} + {appDetail.description && ( +
{appDetail.description}
+ )} + {/* operations */} +
+ +
+
{ + setOpen(false) + setShowEditModal(true) + }}> + {t('app.editApp')} +
+ {appDetail.mode !== 'completion' && ( + <> +
{ + setOpen(false) + setShowDuplicateModal(true) + }}> + {t('app.duplicate')} +
+
+ {t('app.export')} +
+ + )} + +
{ + setOpen(false) + setShowConfirmDelete(true) + }}> + + {t('common.operation.delete')} + +
+
+
+
+ {showEditModal && ( + setShowEditModal(false)} + /> + )} + {showDuplicateModal && ( + setShowDuplicateModal(false)} + /> + )} + {showConfirmDelete && ( + setShowConfirmDelete(false)} + onConfirm={onConfirmDelete} + onCancel={() => setShowConfirmDelete(false)} + /> + )} +
+
+ ) +} + +export default React.memo(AppInfo) diff --git a/web/app/components/app-sidebar/index.tsx b/web/app/components/app-sidebar/index.tsx index 163920321c..d039d7e3c4 100644 --- a/web/app/components/app-sidebar/index.tsx +++ b/web/app/components/app-sidebar/index.tsx @@ -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'} `} > - + {iconType === 'app' && ( + + )} + {iconType !== 'app' && ( + + )}
- +
@@ -112,7 +113,7 @@ const CreateAppModal = ({ setShowEmojiPicker(false) }} onClose={() => { - setEmoji({ icon: '🤖', icon_background: '#FFEAD5' }) + setEmoji({ icon: appIcon, icon_background: appIconBackground }) setShowEmojiPicker(false) }} />} diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index dab2b10893..933a02a6ef 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -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', diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts index 2ec8bc5411..70bdf279e6 100644 --- a/web/i18n/zh-Hans/app.ts +++ b/web/i18n/zh-Hans/app.ts @@ -57,9 +57,10 @@ const translation = { appCreated: '应用已创建', appCreateFailed: '应用创建失败', }, - editApp: { - startToEdit: '编辑应用', - }, + editApp: '编辑信息', + editAppTitle: '编辑应用信息', + editDone: '应用信息已更新', + editFailed: '更新应用信息失败', emoji: { ok: '确认', cancel: '取消', diff --git a/web/models/explore.ts b/web/models/explore.ts index 739c325cfc..ec3ee33a69 100644 --- a/web/models/explore.ts +++ b/web/models/explore.ts @@ -1,11 +1,11 @@ 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' diff --git a/web/service/apps.ts b/web/service/apps.ts index abc3a73e07..573bfd4b4e 100644 --- a/web/service/apps.ts +++ b/web/service/apps.ts @@ -1,5 +1,5 @@ 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('apps', { body: { name, icon, icon_background, mode, description, model_config: config } }) } +export const updateAppInfo: Fetcher = ({ appID, name, icon, icon_background, description }) => { + return put(`apps/${appID}`, { body: { name, icon, icon_background, description } }) +} + export const copyApp: Fetcher = ({ appID, name, icon, icon_background, mode, description }) => { return post(`apps/${appID}/copy`, { body: { name, icon, icon_background, mode, description } }) }