feat: support app rename and make app card ui better (#766)

Co-authored-by: Gillian97 <jinling.sunshine@gmail.com>
This commit is contained in:
Joel
2023-08-16 10:31:08 +08:00
committed by GitHub
parent 216fc5d312
commit f95f6db0e3
53 changed files with 612 additions and 419 deletions

View File

@@ -7,31 +7,21 @@ import useSWR, { useSWRConfig } from 'swr'
import AppCard from '@/app/components/app/overview/appCard'
import Loading from '@/app/components/base/loading'
import { ToastContext } from '@/app/components/base/toast'
import { fetchAppDetail, updateAppApiStatus, updateAppSiteAccessToken, updateAppSiteConfig, updateAppSiteStatus } from '@/service/apps'
import type { IToastProps } from '@/app/components/base/toast'
import {
fetchAppDetail,
updateAppSiteAccessToken,
updateAppSiteConfig,
updateAppSiteStatus,
} from '@/service/apps'
import type { App } from '@/types/app'
import type { UpdateAppSiteCodeResponse } from '@/models/app'
import { asyncRunSafe } from '@/utils'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
export type ICardViewProps = {
appId: string
}
type IParams = {
url: string
body?: Record<string, any>
}
export async function asyncRunSafe<T>(func: (val: IParams) => Promise<T>, params: IParams, callback: (props: IToastProps) => void, dict?: any): Promise<[string?, T?]> {
try {
const res = await func(params)
callback && callback({ type: 'success', message: dict('common.actionMsg.modifiedSuccessfully') })
return [undefined, res]
}
catch (err) {
callback && callback({ type: 'error', message: dict('common.actionMsg.modificationFailed') })
return [(err as Error).message, undefined]
}
}
const CardView: FC<ICardViewProps> = ({ appId }) => {
const detailParams = { url: '/apps', id: appId }
const { data: response } = useSWR(detailParams, fetchAppDetail)
@@ -42,44 +32,78 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
if (!response)
return <Loading />
const onChangeSiteStatus = async (value: boolean) => {
const [err] = await asyncRunSafe<App>(updateAppSiteStatus as any, { url: `/apps/${appId}/site-enable`, body: { enable_site: value } }, notify, t)
if (!err)
const handleError = (err: Error | null) => {
if (!err) {
notify({
type: 'success',
message: t('common.actionMsg.modifiedSuccessfully'),
})
mutate(detailParams)
}
else {
notify({
type: 'error',
message: t('common.actionMsg.modificationFailed'),
})
}
}
const onChangeSiteStatus = async (value: boolean) => {
const [err] = await asyncRunSafe<App>(
updateAppSiteStatus({
url: `/apps/${appId}/site-enable`,
body: { enable_site: value },
}) as Promise<App>,
)
handleError(err)
}
const onChangeApiStatus = async (value: boolean) => {
const [err] = await asyncRunSafe<App>(updateAppApiStatus as any, { url: `/apps/${appId}/api-enable`, body: { enable_api: value } }, notify, t)
if (!err)
mutate(detailParams)
const [err] = await asyncRunSafe<App>(
updateAppSiteStatus({
url: `/apps/${appId}/api-enable`,
body: { enable_api: value },
}) as Promise<App>,
)
handleError(err)
}
const onSaveSiteConfig = async (params: any) => {
const [err] = await asyncRunSafe<App>(updateAppSiteConfig as any, { url: `/apps/${appId}/site`, body: params }, notify, t)
const [err] = await asyncRunSafe<App>(
updateAppSiteConfig({
url: `/apps/${appId}/site`,
body: params,
}) as Promise<App>,
)
if (!err)
mutate(detailParams)
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
handleError(err)
}
const onGenerateCode = async () => {
const [err] = await asyncRunSafe<App>(updateAppSiteAccessToken as any, { url: `/apps/${appId}/site/access-token-reset` }, notify, t)
if (!err)
mutate(detailParams)
const [err] = await asyncRunSafe<UpdateAppSiteCodeResponse>(
updateAppSiteAccessToken({
url: `/apps/${appId}/site/access-token-reset`,
}) as Promise<UpdateAppSiteCodeResponse>,
)
handleError(err)
}
return (
<div className='flex flex-row justify-between w-full mb-6'>
<div className="min-w-max grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6">
<AppCard
className='mr-3 flex-1'
appInfo={response}
cardType='webapp'
cardType="webapp"
onChangeStatus={onChangeSiteStatus}
onGenerateCode={onGenerateCode}
onSaveSiteConfig={onSaveSiteConfig} />
onSaveSiteConfig={onSaveSiteConfig}
/>
<AppCard
className='ml-3 flex-1'
cardType='api'
cardType="api"
appInfo={response}
onChangeStatus={onChangeApiStatus} />
onChangeStatus={onChangeApiStatus}
/>
</div>
)
}

View File

@@ -46,35 +46,23 @@ export default function ChartView({ appId }: IChartViewProps) {
defaultValue={7}
/>
</div>
<div className='flex flex-row w-full mb-6'>
<div className='flex-1 mr-3'>
<ConversationsChart period={period} id={appId} />
</div>
<div className='flex-1 ml-3'>
<EndUsersChart period={period} id={appId} />
</div>
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<ConversationsChart period={period} id={appId} />
<EndUsersChart period={period} id={appId} />
</div>
<div className='flex flex-row w-full mb-6'>
<div className='flex-1 mr-3'>
{isChatApp
? (
<AvgSessionInteractions period={period} id={appId} />
)
: (
<AvgResponseTime period={period} id={appId} />
)}
</div>
<div className='flex-1 ml-3'>
<TokenPerSecond period={period} id={appId} />
</div>
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
{isChatApp
? (
<AvgSessionInteractions period={period} id={appId} />
)
: (
<AvgResponseTime period={period} id={appId} />
)}
<TokenPerSecond period={period} id={appId} />
</div>
<div className='flex flex-row w-full mb-6'>
<div className='flex-1 ml-3'>
<UserSatisfactionRate period={period} id={appId} />
</div>
<div className='flex-1 ml-3'>
<CostChart period={period} id={appId} />
</div>
<div className='grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'>
<UserSatisfactionRate period={period} id={appId} />
<CostChart period={period} id={appId} />
</div>
</div>
)

View File

@@ -2,64 +2,165 @@
import { useContext, useContextSelector } from 'use-context-selector'
import Link from 'next/link'
import type { MouseEventHandler } from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import style from '../list.module.css'
import AppModeLabel from './AppModeLabel'
import s from './style.module.css'
import SettingsModal from '@/app/components/app/overview/settings'
import type { App } from '@/types/app'
import Confirm from '@/app/components/base/confirm'
import { ToastContext } from '@/app/components/base/toast'
import { deleteApp } from '@/service/apps'
import { deleteApp, fetchAppDetail, updateAppSiteConfig } from '@/service/apps'
import AppIcon from '@/app/components/base/app-icon'
import AppsContext, { useAppContext } from '@/context/app-context'
import CustomPopover from '@/app/components/base/popover'
import Divider from '@/app/components/base/divider'
import { asyncRunSafe } from '@/utils'
export type AppCardProps = {
app: App
onDelete?: () => void
onRefresh?: () => void
}
const AppCard = ({
app,
onDelete,
}: AppCardProps) => {
const AppCard = ({ app, onRefresh }: AppCardProps) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const { isCurrentWorkspaceManager } = useAppContext()
const mutateApps = useContextSelector(AppsContext, state => state.mutateApps)
const mutateApps = useContextSelector(
AppsContext,
state => state.mutateApps,
)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const onDeleteClick: MouseEventHandler = useCallback((e) => {
e.preventDefault()
setShowConfirmDelete(true)
}, [])
const [showSettingsModal, setShowSettingsModal] = useState(false)
const [detailState, setDetailState] = useState<{
loading: boolean
detail?: App
}>({ loading: false })
const onConfirmDelete = useCallback(async () => {
try {
await deleteApp(app.id)
notify({ type: 'success', message: t('app.appDeleted') })
if (onDelete)
onDelete()
if (onRefresh)
onRefresh()
mutateApps()
}
catch (e: any) {
notify({ type: 'error', message: `${t('app.appDeleteFailed')}${'message' in e ? `: ${e.message}` : ''}` })
notify({
type: 'error',
message: `${t('app.appDeleteFailed')}${
'message' in e ? `: ${e.message}` : ''
}`,
})
}
setShowConfirmDelete(false)
}, [app.id])
const getAppDetail = async () => {
setDetailState({ loading: true })
const [err, res] = await asyncRunSafe<App>(
fetchAppDetail({ url: '/apps', id: app.id }) as Promise<App>,
)
if (!err) {
setDetailState({ loading: false, detail: res })
setShowSettingsModal(true)
}
else { setDetailState({ loading: false }) }
}
const onSaveSiteConfig = useCallback(
async (params: any) => {
const [err] = await asyncRunSafe<App>(
updateAppSiteConfig({
url: `/apps/${app.id}/site`,
body: params,
}) as Promise<App>,
)
if (!err) {
notify({
type: 'success',
message: t('common.actionMsg.modifiedSuccessfully'),
})
if (onRefresh)
onRefresh()
mutateApps()
}
else {
notify({
type: 'error',
message: t('common.actionMsg.modificationFailed'),
})
}
},
[app.id],
)
const Operations = (props: any) => {
const onClickSettings = async (e: any) => {
props?.onClose()
e.preventDefault()
await getAppDetail()
}
const onClickDelete = async (e: any) => {
props?.onClose()
e.preventDefault()
setShowConfirmDelete(true)
}
return (
<div className="w-full py-1">
<button className={s.actionItem} onClick={onClickSettings} disabled={detailState.loading}>
<span className={s.actionName}>{t('common.operation.settings')}</span>
</button>
<Divider className="!my-1" />
<div
className={cn(s.actionItem, s.deleteActionItem, 'group')}
onClick={onClickDelete}
>
<span className={cn(s.actionName, 'group-hover:text-red-500')}>
{t('common.operation.delete')}
</span>
</div>
</div>
)
}
return (
<>
<Link href={`/app/${app.id}/overview`} className={style.listItem}>
<Link
href={`/app/${app.id}/overview`}
className={style.listItem}
>
<div className={style.listItemTitle}>
<AppIcon size='small' icon={app.icon} background={app.icon_background} />
<AppIcon
size="small"
icon={app.icon}
background={app.icon_background}
/>
<div className={style.listItemHeading}>
<div className={style.listItemHeadingContent}>{app.name}</div>
</div>
{ isCurrentWorkspaceManager
&& <span className={style.deleteAppIcon} onClick={onDeleteClick} />}
{isCurrentWorkspaceManager && <CustomPopover
htmlContent={<Operations />}
position="br"
trigger="click"
btnElement={<div className={cn(s.actionIcon, s.commonIcon)} />}
btnClassName={open =>
cn(
open ? '!bg-gray-100 !shadow-none' : '!bg-transparent',
style.actionIconWrapper,
)
}
className={'!w-[128px] h-fit !z-20'}
/>}
</div>
<div className={style.listItemDescription}>
{app.model_config?.pre_prompt}
</div>
<div className={style.listItemDescription}>{app.model_config?.pre_prompt}</div>
<div className={style.listItemFooter}>
<AppModeLabel mode={app.mode} />
</div>
@@ -74,6 +175,14 @@ const AppCard = ({
onCancel={() => setShowConfirmDelete(false)}
/>
)}
{showSettingsModal && detailState.detail && (
<SettingsModal
appInfo={detailState.detail}
isShow={showSettingsModal}
onClose={() => setShowSettingsModal(false)}
onSave={onSaveSiteConfig}
/>
)}
</Link>
</>
)

View File

@@ -2,8 +2,8 @@
import classNames from 'classnames'
import { useTranslation } from 'react-i18next'
import { type AppMode } from '@/types/app'
import style from '../list.module.css'
import { type AppMode } from '@/types/app'
export type AppModeLabelProps = {
mode: AppMode

View File

@@ -54,7 +54,7 @@ const Apps = () => {
return (
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-8 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
{data?.map(({ data: apps }) => apps.map(app => (
<AppCard key={app.id} app={app} onDelete={mutate} />
<AppCard key={app.id} app={app} onRefresh={mutate} />
)))}
{ isCurrentWorkspaceManager
&& <NewAppCard ref={anchorRef} onSuccess={mutate} />}

View File

@@ -119,7 +119,7 @@ const NewAppDialog = ({ show, onSuccess, onClose }: NewAppDialogProps) => {
<div className='flex items-center justify-between gap-3 mb-8'>
<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' />
<input ref={nameInputRef} className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow' placeholder={t('app.appNamePlaceholder') || ''}/>
</div>
<div className='h-[247px]'>

View File

@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.6665 7.33333V11.3333M9.33317 7.33333V11.3333M2.6665 4.66667H13.3332M12.6665 4.66667L12.0885 12.7613C12.0646 13.0977 11.914 13.4125 11.6672 13.6424C11.4205 13.8722 11.0957 14 10.7585 14H5.24117C4.90393 14 4.57922 13.8722 4.33243 13.6424C4.08564 13.4125 3.93511 13.0977 3.91117 12.7613L3.33317 4.66667H12.6665ZM9.99984 4.66667V2.66667C9.99984 2.48986 9.9296 2.32029 9.80457 2.19526C9.67955 2.07024 9.50998 2 9.33317 2H6.6665C6.48969 2 6.32012 2.07024 6.1951 2.19526C6.07008 2.32029 5.99984 2.48986 5.99984 2.66667V4.66667H9.99984Z" stroke="#1F2A37" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 711 B

View File

@@ -0,0 +1,21 @@
.commonIcon {
@apply w-4 h-4 inline-block align-middle;
background-repeat: no-repeat;
background-position: center center;
background-size: contain;
}
.actionIcon {
@apply bg-gray-500;
mask-image: url(~@/assets/action.svg);
}
.actionItem {
@apply h-9 py-2 px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer;
width: calc(100% - 0.5rem);
}
.deleteActionItem {
@apply hover:bg-red-50 !important;
}
.actionName {
@apply text-gray-700 text-sm;
}

View File

@@ -51,7 +51,7 @@ const DatasetCard = ({
<div className={style.listItemHeading}>
<div className={style.listItemHeadingContent}>{dataset.name}</div>
</div>
<span className={style.deleteAppIcon} onClick={onDeleteClick} />
<span className={style.deleteDatasetIcon} onClick={onDeleteClick} />
</div>
<div className={style.listItemDescription}>{dataset.description}</div>
<div className={classNames(style.listItemFooter, style.datasetCardFooter)}>

View File

@@ -14,9 +14,14 @@
@apply relative;
}
.listItem.selectable::before {
content: '';
content: "";
@apply absolute top-0 left-0 block w-full h-full rounded-lg pointer-events-none opacity-0 transition-opacity duration-200 ease-in-out hover:opacity-100;
background: linear-gradient(0deg, rgba(235, 245, 255, 0.5), rgba(235, 245, 255, 0.5)), #FFFFFF;
background: linear-gradient(
0deg,
rgba(235, 245, 255, 0.5),
rgba(235, 245, 255, 0.5)
),
#ffffff;
}
.listItem.selectable:hover::before {
@apply opacity-100;
@@ -65,13 +70,13 @@
@apply text-primary-600;
}
.newItemIconAdd {
background-image: url('./apps/assets/add.svg');
background-image: url("./apps/assets/add.svg");
}
.newItemIconChat {
background-image: url('./apps/assets/chat.svg');
background-image: url("./apps/assets/chat.svg");
}
.newItemIconComplete {
background-image: url('./apps/assets/completion.svg');
background-image: url("./apps/assets/completion.svg");
}
.listItemTitle {
@@ -86,12 +91,18 @@
@apply absolute top-0 left-0 w-full h-full overflow-hidden text-ellipsis whitespace-nowrap;
}
.deleteAppIcon {
.actionIconWrapper {
@apply hidden h-8 w-8 p-2 rounded-md border-none hover:bg-gray-100 !important;
}
.listItem:hover .actionIconWrapper {
@apply !inline-flex;
}
.deleteDatasetIcon {
@apply hidden grow-0 shrink-0 basis-8 w-8 h-8 rounded-lg transition-colors duration-200 ease-in-out bg-white border border-gray-200 hover:bg-gray-100 bg-center bg-no-repeat;
background-size: 16px;
background-image: url('./apps/assets/delete.svg');
background-image: url('~@/assets/delete.svg');
}
.listItem:hover .deleteAppIcon {
.listItem:hover .deleteDatasetIcon {
@apply block;
}
@@ -114,19 +125,19 @@
@apply block w-3 h-3 bg-center bg-contain;
}
.solidChatIcon {
background-image: url('./apps/assets/chat-solid.svg');
background-image: url("./apps/assets/chat-solid.svg");
}
.solidCompletionIcon {
background-image: url('./apps/assets/completion-solid.svg');
background-image: url("./apps/assets/completion-solid.svg");
}
.docIcon {
background-image: url('./datasets/assets/doc.svg');
background-image: url("./datasets/assets/doc.svg");
}
.textIcon {
background-image: url('./datasets/assets/text.svg');
background-image: url("./datasets/assets/text.svg");
}
.applicationIcon {
background-image: url('./datasets/assets/application.svg');
background-image: url("./datasets/assets/application.svg");
}
.newItemCardHeading {
@@ -140,24 +151,24 @@
@apply inline-flex items-center gap-1 text-xs text-gray-400 transition-colors duration-200 ease-in-out;
}
.listItem:hover .listItemLink {
@apply text-primary-600
@apply text-primary-600;
}
.linkIcon {
@apply block w-[13px] h-[13px] bg-center bg-contain;
background-image: url('./apps/assets/link.svg');
background-image: url("./apps/assets/link.svg");
}
.linkIcon.grayLinkIcon {
background-image: url('./apps/assets/link-gray.svg');
background-image: url("./apps/assets/link-gray.svg");
}
.listItem:hover .grayLinkIcon {
background-image: url('./apps/assets/link.svg');
background-image: url("./apps/assets/link.svg");
}
.rightIcon {
@apply block w-[13px] h-[13px] bg-center bg-contain;
background-image: url('./apps/assets/right-arrow.svg');
background-image: url("./apps/assets/right-arrow.svg");
}
.socialMediaLink {
@@ -169,11 +180,11 @@
}
.githubIcon {
background-image: url('./apps/assets/github.svg');
background-image: url("./apps/assets/github.svg");
}
.discordIcon {
background-image: url('./apps/assets/discord.svg');
background-image: url("./apps/assets/discord.svg");
}
/* #region new app dialog */