feat: support app rename and make app card ui better (#766)
Co-authored-by: Gillian97 <jinling.sunshine@gmail.com>
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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} />}
|
||||
|
|
|
@ -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]'>
|
||||
|
|
21
web/app/(commonLayout)/apps/style.module.css
Normal 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;
|
||||
}
|
|
@ -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)}>
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -4,13 +4,7 @@ import {
|
|||
} from '@heroicons/react/24/outline'
|
||||
import Tooltip from '../base/tooltip'
|
||||
import AppIcon from '../base/app-icon'
|
||||
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'
|
||||
|
||||
export function randomString(length: number) {
|
||||
let result = ''
|
||||
for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)]
|
||||
return result
|
||||
}
|
||||
import { randomString } from '@/utils'
|
||||
|
||||
export type IAppBasicProps = {
|
||||
iconType?: 'app' | 'api' | 'dataset' | 'webapp' | 'notion'
|
||||
|
|
|
@ -6,7 +6,6 @@ import { useContext } from 'use-context-selector'
|
|||
import { UserCircleIcon } from '@heroicons/react/24/solid'
|
||||
import cn from 'classnames'
|
||||
import type { DisplayScene, FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc, ThoughtItem } from '../type'
|
||||
import { randomString } from '../../../app-sidebar/basic'
|
||||
import OperationBtn from '../operation'
|
||||
import LoadingAnim from '../loading-anim'
|
||||
import { EditIcon, EditIconSolid, OpeningStatementIcon, RatingIcon } from '../icon-component'
|
||||
|
@ -14,6 +13,7 @@ import s from '../style.module.css'
|
|||
import MoreInfo from '../more-info'
|
||||
import CopyBtn from '../copy-btn'
|
||||
import Thought from '../thought'
|
||||
import { randomString } from '@/utils'
|
||||
import type { Annotation, MessageRating } from '@/models/log'
|
||||
import AppContext from '@/context/app-context'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
|
|
@ -16,8 +16,8 @@ import dayjs from 'dayjs'
|
|||
import { createContext, useContext } from 'use-context-selector'
|
||||
import classNames from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { randomString } from '../../app-sidebar/basic'
|
||||
import s from './style.module.css'
|
||||
import { randomString } from '@/utils'
|
||||
import { EditIconSolid } from '@/app/components/app/chat/icon-component'
|
||||
import type { FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc } from '@/app/components/app/chat/type'
|
||||
import type { Annotation, ChatConversationFullDetailResponse, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationFullDetailResponse, CompletionConversationGeneralDetail, CompletionConversationsResponse } from '@/models/log'
|
||||
|
|
|
@ -4,30 +4,32 @@ import React, { useMemo, useState } from 'react'
|
|||
import {
|
||||
Cog8ToothIcon,
|
||||
DocumentTextIcon,
|
||||
PaintBrushIcon,
|
||||
RocketLaunchIcon,
|
||||
ShareIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import { SparklesIcon } from '@heroicons/react/24/solid'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SettingsModal from './settings'
|
||||
import ShareLink from './share-link'
|
||||
import EmbeddedModal from './embedded'
|
||||
import CustomizeModal from './customize'
|
||||
import style from './style.module.css'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import AppBasic, { randomString } from '@/app/components/app-sidebar/basic'
|
||||
import AppBasic from '@/app/components/app-sidebar/basic'
|
||||
import { asyncRunSafe, randomString } from '@/utils'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Tag from '@/app/components/base/tag'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import CopyFeedback from '@/app/components/base/copy-feedback'
|
||||
import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button'
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import './style.css'
|
||||
import { AppType } from '@/types/app'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
||||
export type IAppCardProps = {
|
||||
className?: string
|
||||
appInfo: AppDetailResponse
|
||||
cardType?: 'app' | 'api' | 'webapp'
|
||||
cardType?: 'api' | 'webapp'
|
||||
customBgColor?: string
|
||||
onChangeStatus: (val: boolean) => Promise<any>
|
||||
onSaveSiteConfig?: (params: any) => Promise<any>
|
||||
|
@ -35,12 +37,12 @@ export type IAppCardProps = {
|
|||
}
|
||||
|
||||
const EmbedIcon: FC<{ className?: string }> = ({ className = '' }) => {
|
||||
return <div className={`codeBrowserIcon ${className}`}></div>
|
||||
return <div className={`${style.codeBrowserIcon} ${className}`}></div>
|
||||
}
|
||||
|
||||
function AppCard({
|
||||
appInfo,
|
||||
cardType = 'app',
|
||||
cardType = 'webapp',
|
||||
customBgColor,
|
||||
onChangeStatus,
|
||||
onSaveSiteConfig,
|
||||
|
@ -51,16 +53,16 @@ function AppCard({
|
|||
const pathname = usePathname()
|
||||
const { currentWorkspace, isCurrentWorkspaceManager } = useAppContext()
|
||||
const [showSettingsModal, setShowSettingsModal] = useState(false)
|
||||
const [showShareModal, setShowShareModal] = useState(false)
|
||||
const [showEmbedded, setShowEmbedded] = useState(false)
|
||||
const [showCustomizeModal, setShowCustomizeModal] = useState(false)
|
||||
const [genLoading, setGenLoading] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const OPERATIONS_MAP = useMemo(() => {
|
||||
const operationsMap = {
|
||||
webapp: [
|
||||
{ opName: t('appOverview.overview.appInfo.preview'), opIcon: RocketLaunchIcon },
|
||||
{ opName: t('appOverview.overview.appInfo.share.entry'), opIcon: ShareIcon },
|
||||
{ opName: t('appOverview.overview.appInfo.customize.entry'), opIcon: PaintBrushIcon },
|
||||
] as { opName: string; opIcon: any }[],
|
||||
api: [{ opName: t('appOverview.overview.apiInfo.doc'), opIcon: DocumentTextIcon }],
|
||||
app: [],
|
||||
|
@ -74,8 +76,10 @@ function AppCard({
|
|||
return operationsMap
|
||||
}, [isCurrentWorkspaceManager, appInfo, t])
|
||||
|
||||
const isApp = cardType === 'app' || cardType === 'webapp'
|
||||
const basicName = isApp ? appInfo?.site?.title : t('appOverview.overview.apiInfo.title')
|
||||
const isApp = cardType === 'webapp'
|
||||
const basicName = isApp
|
||||
? appInfo?.site?.title
|
||||
: t('appOverview.overview.apiInfo.title')
|
||||
const runningStatus = isApp ? appInfo.enable_site : appInfo.enable_api
|
||||
const { app_base_url, access_token } = appInfo.site ?? {}
|
||||
const appUrl = `${app_base_url}/${appInfo.mode}/${access_token}`
|
||||
|
@ -91,9 +95,9 @@ function AppCard({
|
|||
return () => {
|
||||
window.open(appUrl, '_blank')
|
||||
}
|
||||
case t('appOverview.overview.appInfo.share.entry'):
|
||||
case t('appOverview.overview.appInfo.customize.entry'):
|
||||
return () => {
|
||||
setShowShareModal(true)
|
||||
setShowCustomizeModal(true)
|
||||
}
|
||||
case t('appOverview.overview.appInfo.settings.entry'):
|
||||
return () => {
|
||||
|
@ -113,15 +117,19 @@ function AppCard({
|
|||
}
|
||||
}
|
||||
|
||||
const onClickCustomize = () => {
|
||||
setShowCustomizeModal(true)
|
||||
const onGenCode = async () => {
|
||||
setGenLoading(true)
|
||||
await asyncRunSafe(onGenerateCode?.() as any)
|
||||
setGenLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col w-full shadow-xs border-[0.5px] rounded-lg border-gray-200 ${className ?? ''}`}
|
||||
className={`min-w-max shadow-xs border-[0.5px] rounded-lg border-gray-200 ${
|
||||
className ?? ''
|
||||
}`}
|
||||
>
|
||||
<div className={`px-6 py-4 ${customBgColor ?? bgColor} rounded-lg`}>
|
||||
<div className={`px-6 py-5 ${customBgColor ?? bgColor} rounded-lg`}>
|
||||
<div className="mb-2.5 flex flex-row items-start justify-between">
|
||||
<AppBasic
|
||||
iconType={cardType}
|
||||
|
@ -136,7 +144,9 @@ function AppCard({
|
|||
/>
|
||||
<div className="flex flex-row items-center h-9">
|
||||
<Tag className="mr-2" color={runningStatus ? 'green' : 'yellow'}>
|
||||
{runningStatus ? t('appOverview.overview.status.running') : t('appOverview.overview.status.disable')}
|
||||
{runningStatus
|
||||
? t('appOverview.overview.status.running')
|
||||
: t('appOverview.overview.status.disable')}
|
||||
</Tag>
|
||||
<Switch defaultValue={runningStatus} onChange={onChangeStatus} disabled={currentWorkspace?.role === 'normal'} />
|
||||
</div>
|
||||
|
@ -144,39 +154,67 @@ function AppCard({
|
|||
<div className="flex flex-col justify-center py-2">
|
||||
<div className="py-1">
|
||||
<div className="pb-1 text-xs text-gray-500">
|
||||
{isApp ? t('appOverview.overview.appInfo.accessibleAddress') : t('appOverview.overview.apiInfo.accessibleAddress')}
|
||||
{isApp
|
||||
? t('appOverview.overview.appInfo.accessibleAddress')
|
||||
: t('appOverview.overview.apiInfo.accessibleAddress')}
|
||||
</div>
|
||||
<div className="text-sm text-gray-800">
|
||||
{isApp ? appUrl : apiUrl}
|
||||
<div className="w-full h-9 pl-2 pr-0.5 py-0.5 bg-black bg-opacity-[0.02] rounded-lg border border-black border-opacity-5 justify-start items-center inline-flex">
|
||||
<div className="h-4 px-2 justify-start items-start gap-2 flex flex-1">
|
||||
<div className="text-gray-700 text-xs font-medium">
|
||||
{isApp ? appUrl : apiUrl}
|
||||
</div>
|
||||
</div>
|
||||
<Divider type="vertical" className="!h-3.5 shrink-0 !mx-0.5" />
|
||||
<CopyFeedback
|
||||
content={isApp ? appUrl : apiUrl}
|
||||
selectorId={randomString(8)}
|
||||
className={'hover:bg-gray-200'}
|
||||
/>
|
||||
{/* button copy link/ button regenerate */}
|
||||
{isApp && isCurrentWorkspaceManager && (
|
||||
<Tooltip
|
||||
content={t('appOverview.overview.appInfo.regenerate') || ''}
|
||||
selector={`code-generate-${randomString(8)}`}
|
||||
>
|
||||
<div
|
||||
className="w-8 h-8 ml-0.5 cursor-pointer hover:bg-gray-200 rounded-lg"
|
||||
onClick={onGenCode}
|
||||
>
|
||||
<div
|
||||
className={`w-full h-full ${style.refreshIcon} ${
|
||||
genLoading ? style.generateLogo : ''
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`pt-2 flex flex-row items-center ${!isApp ? 'mb-[calc(2rem_+_1px)]' : ''
|
||||
}`}
|
||||
>
|
||||
{OPERATIONS_MAP[cardType].map((op) => {
|
||||
<div className={'pt-2 flex flex-row items-center'}>
|
||||
{!isApp && <SecretKeyButton className='flex-shrink-0 !h-8 bg-white mr-2' textCls='!text-gray-700 font-medium' iconCls='stroke-[1.2px]' appId={appInfo.id} />}
|
||||
{OPERATIONS_MAP[cardType].map((op: any) => {
|
||||
const disabled
|
||||
= op.opName === t('appOverview.overview.appInfo.settings.entry')
|
||||
? false
|
||||
: !runningStatus
|
||||
return (
|
||||
<Button
|
||||
className="mr-2 text-gray-800"
|
||||
className="mr-2 border-[0.5px] !h-8 hover:outline hover:outline-[0.5px] hover:outline-gray-300 text-gray-700 font-medium bg-white shadow-[0px_1px_2px_0px_rgba(16,24,40,0.05)]"
|
||||
key={op.opName}
|
||||
onClick={genClickFuncByName(op.opName)}
|
||||
disabled={
|
||||
[t('appOverview.overview.appInfo.preview'), t('appOverview.overview.appInfo.share.entry'), t('appOverview.overview.appInfo.embedded.entry')].includes(op.opName) && !runningStatus
|
||||
}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Tooltip
|
||||
content={t('appOverview.overview.appInfo.preUseReminder') ?? ''}
|
||||
selector={`op-btn-${randomString(16)}`}
|
||||
className={
|
||||
([t('appOverview.overview.appInfo.preview'), t('appOverview.overview.appInfo.share.entry'), t('appOverview.overview.appInfo.embedded.entry')].includes(op.opName) && !runningStatus)
|
||||
? 'mt-[-8px]'
|
||||
: '!hidden'
|
||||
content={
|
||||
t('appOverview.overview.appInfo.preUseReminder') ?? ''
|
||||
}
|
||||
selector={`op-btn-${randomString(16)}`}
|
||||
className={disabled ? 'mt-[-8px]' : '!hidden'}
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
<op.opIcon className="h-4 w-4 mr-1.5 stroke-[1.8px]" />
|
||||
<span className="text-xs">{op.opName}</span>
|
||||
<span className="text-[13px]">{op.opName}</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
|
@ -186,31 +224,7 @@ function AppCard({
|
|||
</div>
|
||||
{isApp
|
||||
? (
|
||||
<div
|
||||
className={
|
||||
'flex items-center px-6 py-2 box-border text-xs text-gray-500 bg-opacity-50 bg-white border-t-[0.5px] border-primary-50'
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="flex items-center hover:text-primary-600 hover:cursor-pointer"
|
||||
onClick={onClickCustomize}
|
||||
>
|
||||
<SparklesIcon className="w-4 h-4 mr-1" />
|
||||
{t('appOverview.overview.appInfo.customize.entry')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: null}
|
||||
{isApp
|
||||
? (
|
||||
<div>
|
||||
<ShareLink
|
||||
isShow={showShareModal}
|
||||
onClose={() => setShowShareModal(false)}
|
||||
linkUrl={appUrl}
|
||||
onGenerateCode={onGenerateCode}
|
||||
regeneratable={isCurrentWorkspaceManager}
|
||||
/>
|
||||
<>
|
||||
<SettingsModal
|
||||
appInfo={appInfo}
|
||||
isShow={showSettingsModal}
|
||||
|
@ -230,7 +244,7 @@ function AppCard({
|
|||
appId={appInfo.id}
|
||||
mode={appInfo.mode}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
: null}
|
||||
</div>
|
||||
|
|
|
@ -229,7 +229,7 @@ const Chart: React.FC<IChartProps> = ({
|
|||
<div className='mb-3'>
|
||||
<Basic name={title} type={timePeriod} hoverTip={explanation} />
|
||||
</div>
|
||||
<div className='mb-4'>
|
||||
<div className='mb-4 flex-1'>
|
||||
<Basic
|
||||
isExtraInLine={CHART_TYPE_CONFIG[chartType].showTokens}
|
||||
name={chartType !== 'costs' ? (sumData.toLocaleString() + unit) : `${sumData < 1000 ? sumData : (`${formatNumber(Math.round(sumData / 1000))}k`)}`}
|
||||
|
@ -347,6 +347,7 @@ export const UserSatisfactionRate: FC<IBizChartProps> = ({ id, period }) => {
|
|||
chartType='endUsers'
|
||||
isAvg
|
||||
{...(noDataFlag && { yMax: 1000 })}
|
||||
className='h-full'
|
||||
/>
|
||||
}
|
||||
|
||||
|
|
3
web/app/components/app/overview/assets/refresh-hover.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.6353 16.5954C21.4501 18.3353 20.4643 19.9658 18.833 20.9076C16.1226 22.4724 12.6569 21.5438 11.0921 18.8335L10.9255 18.5448M10.3641 15.4047C10.5493 13.6647 11.5352 12.0343 13.1665 11.0924C15.8768 9.5276 19.3425 10.4562 20.9073 13.1666L21.074 13.4552M10.3288 20.044L10.8168 18.2227L12.6382 18.7107M19.3616 13.2893L21.183 13.7774L21.671 11.956" stroke="#1D2939" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 544 B |
3
web/app/components/app/overview/assets/refresh.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.6353 16.5954C21.4501 18.3353 20.4643 19.9658 18.833 20.9076C16.1226 22.4724 12.6569 21.5438 11.0921 18.8335L10.9255 18.5448M10.3641 15.4047C10.5493 13.6647 11.5352 12.0343 13.1665 11.0924C15.8768 9.5276 19.3425 10.4562 20.9073 13.1666L21.074 13.4552M10.3288 20.044L10.8168 18.2227L12.6382 18.7107M19.3616 13.2893L21.183 13.7774L21.671 11.956" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 544 B |
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { ChevronRightIcon } from '@heroicons/react/20/solid'
|
||||
import Link from 'next/link'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
@ -18,7 +18,7 @@ export type ISettingsModalProps = {
|
|||
isShow: boolean
|
||||
defaultValue?: string
|
||||
onClose: () => void
|
||||
onSave: (params: ConfigParams) => Promise<any>
|
||||
onSave?: (params: ConfigParams) => Promise<any>
|
||||
}
|
||||
|
||||
export type ConfigParams = {
|
||||
|
@ -51,6 +51,12 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
|||
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
|
||||
const [emoji, setEmoji] = useState({ icon, icon_background })
|
||||
|
||||
useEffect(() => {
|
||||
setInputInfo({ title, desc: description, copyright, privacyPolicy: privacy_policy })
|
||||
setLanguage(default_language)
|
||||
setEmoji({ icon, icon_background })
|
||||
}, [appInfo])
|
||||
|
||||
const onHide = () => {
|
||||
onClose()
|
||||
setTimeout(() => {
|
||||
|
@ -70,7 +76,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
|||
icon: emoji.icon,
|
||||
icon_background: emoji.icon_background,
|
||||
}
|
||||
await onSave(params)
|
||||
await onSave?.(params)
|
||||
setSaveLoading(false)
|
||||
onHide()
|
||||
}
|
||||
|
@ -99,7 +105,9 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
|||
/>
|
||||
<input className={`flex-grow rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
|
||||
value={inputInfo.title}
|
||||
onChange={onChange('title')} />
|
||||
onChange={onChange('title')}
|
||||
placeholder={t('app.appNamePlaceholder') || ''}
|
||||
/>
|
||||
</div>
|
||||
<div className={`mt-6 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.webDesc`)}</div>
|
||||
<p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.webDescTip`)}</p>
|
||||
|
@ -157,7 +165,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
|||
setShowEmojiPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setEmoji({ icon: '🤖', icon_background: '#FFEAD5' })
|
||||
setEmoji({ icon: appInfo.site.icon, icon_background: appInfo.site.icon_background })
|
||||
setShowEmojiPicker(false)
|
||||
}}
|
||||
/>}
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
LinkIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
import './style.css'
|
||||
|
||||
type IShareLinkProps = {
|
||||
isShow: boolean
|
||||
onClose: () => void
|
||||
onGenerateCode: () => Promise<void>
|
||||
linkUrl: string
|
||||
regeneratable?: boolean
|
||||
}
|
||||
|
||||
const prefixShare = 'appOverview.overview.appInfo.share'
|
||||
|
||||
const ShareLinkModal: FC<IShareLinkProps> = ({
|
||||
linkUrl,
|
||||
isShow,
|
||||
onClose,
|
||||
onGenerateCode,
|
||||
regeneratable,
|
||||
}) => {
|
||||
const [genLoading, setGenLoading] = useState(false)
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
return <Modal
|
||||
title={t(`${prefixShare}.explanation`)}
|
||||
isShow={isShow}
|
||||
onClose={onClose}
|
||||
>
|
||||
{/* share url */}
|
||||
<p className='mt-5 text-xs font-medium text-gray-500'>{t(`${prefixShare}.shareUrl`)}</p>
|
||||
{/* input share url */}
|
||||
<input disabled type='text' value={linkUrl} className='mt-1 w-full bg-gray-50 p-2 text-primary-600 text-xs font-normal outline-gray-50 hover:outline-gray-50 cursor-pointer' />
|
||||
{/* button copy link/ button regenerate */}
|
||||
<div className='mt-4 flex gap-3'>
|
||||
<Button
|
||||
type="primary"
|
||||
className='w-32 !px-0'
|
||||
onClick={() => {
|
||||
copy(linkUrl) && setIsCopied(true)
|
||||
}}
|
||||
>
|
||||
<LinkIcon className='w-4 h-4 mr-2' />
|
||||
{ t(`${prefixShare}.${isCopied ? 'linkCopied' : 'copyLink'}`) }
|
||||
</Button>
|
||||
{regeneratable && <Button className='w-32 !px-0' onClick={async () => {
|
||||
setGenLoading(true)
|
||||
await onGenerateCode()
|
||||
setGenLoading(false)
|
||||
setIsCopied(false)
|
||||
}}>
|
||||
<ArrowPathIcon className={`w-4 h-4 mr-2 ${genLoading ? 'generateLogo' : ''}`} />
|
||||
{t(`${prefixShare}.regenerate`)}
|
||||
</Button>}
|
||||
</div>
|
||||
</Modal>
|
||||
}
|
||||
|
||||
export default ShareLinkModal
|
|
@ -16,3 +16,16 @@
|
|||
@apply w-4 h-4 bg-center bg-no-repeat;
|
||||
background-image: url(./assets/code-browser.svg);
|
||||
}
|
||||
|
||||
.refreshIcon {
|
||||
background-image: url(./assets/refresh.svg);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.refreshIcon:hover {
|
||||
background-image: url(./assets/refresh-hover.svg);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
|
@ -41,8 +41,8 @@ const ConfirmUI: FC<IConfirmUIProps> = ({
|
|||
</div>
|
||||
|
||||
<div className='flex gap-3 mt-4 ml-12'>
|
||||
<div onClick={onConfirm} className='w-20 leading-9 text-center text-white border rounded-lg cursor-pointer h-9 border-color-primary-700 bg-primary-700'>{confirmText || t('common.operation.confirm')}</div>
|
||||
<div onClick={onCancel} className='w-20 leading-9 text-center text-gray-500 border rounded-lg cursor-pointer h-9 border-color-gray-200'>{cancelText || t('common.operation.cancel')}</div>
|
||||
<div onClick={onConfirm} className='w-20 leading-[34px] text-center text-white border rounded-lg cursor-pointer h-9 border-color-primary-700 bg-primary-700'>{confirmText || t('common.operation.confirm')}</div>
|
||||
<div onClick={onCancel} className='w-20 leading-[34px] text-center text-gray-500 border rounded-lg cursor-pointer h-9 border-color-gray-200'>{cancelText || t('common.operation.cancel')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Dialog, Transition } from '@headlessui/react'
|
||||
import { Fragment } from 'react'
|
||||
import ConfirmUI from '../confirm-ui'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ConfirmUI from '../confirm-ui'
|
||||
|
||||
// https://headlessui.com/react/dialog
|
||||
|
||||
|
|
56
web/app/components/base/copy-feedback/index.tsx
Normal file
|
@ -0,0 +1,56 @@
|
|||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { debounce } from 'lodash-es'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import Tooltip from '../tooltip'
|
||||
import copyStyle from './style.module.css'
|
||||
|
||||
type Props = {
|
||||
content: string
|
||||
selectorId: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
const prefixEmbedded = 'appOverview.overview.appInfo.embedded'
|
||||
|
||||
const CopyFeedback = ({ content, selectorId, className }: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false)
|
||||
|
||||
const onClickCopy = debounce(() => {
|
||||
copy(content)
|
||||
setIsCopied(true)
|
||||
}, 100)
|
||||
|
||||
const onMouseLeave = debounce(() => {
|
||||
setIsCopied(false)
|
||||
}, 100)
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
selector={`common-copy-feedback-${selectorId}`}
|
||||
content={
|
||||
(isCopied
|
||||
? t(`${prefixEmbedded}.copied`)
|
||||
: t(`${prefixEmbedded}.copy`)) || ''
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg ${
|
||||
className ?? ''
|
||||
}`}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<div
|
||||
onClick={onClickCopy}
|
||||
className={`w-full h-full ${copyStyle.copyIcon} ${
|
||||
isCopied ? copyStyle.copied : ''
|
||||
}`}
|
||||
></div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyFeedback
|
15
web/app/components/base/copy-feedback/style.module.css
Normal file
|
@ -0,0 +1,15 @@
|
|||
.copyIcon {
|
||||
background-image: url(~@/app/components/develop/secret-key/assets/copy.svg);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.copyIcon:hover {
|
||||
background-image: url(~@/app/components/develop/secret-key/assets/copy-hover.svg);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.copyIcon.copied {
|
||||
background-image: url(~@/app/components/develop/secret-key/assets/copied.svg);
|
||||
}
|
|
@ -46,47 +46,49 @@ export default function CustomPopover({
|
|||
: {
|
||||
onMouseLeave: () => onMouseLeave(open),
|
||||
onMouseEnter: () => onMouseEnter(open),
|
||||
})
|
||||
}
|
||||
})}
|
||||
>
|
||||
<Popover.Button
|
||||
ref={buttonRef}
|
||||
className={`group ${s.popupBtn} ${open ? '' : 'bg-gray-100'} ${!btnClassName ? '' : typeof btnClassName === 'string' ? btnClassName : btnClassName?.(open)}`}
|
||||
className={`group ${s.popupBtn} ${open ? '' : 'bg-gray-100'} ${
|
||||
!btnClassName
|
||||
? ''
|
||||
: typeof btnClassName === 'string'
|
||||
? btnClassName
|
||||
: btnClassName?.(open)
|
||||
}`}
|
||||
>
|
||||
{btnElement}
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Transition as={Fragment}>
|
||||
<Popover.Panel
|
||||
className={`${s.popupPanel} ${position === 'br' ? 'right-0' : 'transform -translate-x-1/2 left-1/2'} ${className}`}
|
||||
className={`${s.popupPanel} ${
|
||||
position === 'br'
|
||||
? 'right-0'
|
||||
: 'transform -translate-x-1/2 left-1/2'
|
||||
} ${className}`}
|
||||
{...(trigger !== 'hover'
|
||||
? {}
|
||||
: {
|
||||
onMouseLeave: () => onMouseLeave(open),
|
||||
onMouseEnter: () => onMouseEnter(open),
|
||||
})
|
||||
}>
|
||||
<div
|
||||
className={s.panelContainer}
|
||||
{...(trigger !== 'hover'
|
||||
? {}
|
||||
: {
|
||||
onMouseLeave: () => onMouseLeave(open),
|
||||
onMouseEnter: () => onMouseEnter(open),
|
||||
})
|
||||
}
|
||||
>
|
||||
{cloneElement(htmlContent as React.ReactElement, {
|
||||
onClose: () => onMouseLeave(open),
|
||||
})}
|
||||
</div>
|
||||
>
|
||||
{({ close }) => (
|
||||
<div
|
||||
className={s.panelContainer}
|
||||
{...(trigger !== 'hover'
|
||||
? {}
|
||||
: {
|
||||
onMouseLeave: () => onMouseLeave(open),
|
||||
onMouseEnter: () => onMouseEnter(open),
|
||||
})}
|
||||
>
|
||||
{cloneElement(htmlContent as React.ReactElement, {
|
||||
onClose: () => close(),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
|
|
|
@ -138,10 +138,74 @@ export const OperationAction: FC<{
|
|||
onUpdate(operationName)
|
||||
}
|
||||
|
||||
return <div
|
||||
className='flex items-center'
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
const Operations = (props: any) => <div className='w-full py-1'>
|
||||
{!isListScene && <>
|
||||
<div className='flex justify-between items-center mx-4 pt-2'>
|
||||
<span className={cn(s.actionName, 'font-medium')}>
|
||||
{!archived && enabled ? t('datasetDocuments.list.index.enable') : t('datasetDocuments.list.index.disable')}
|
||||
</span>
|
||||
<Tooltip
|
||||
selector={`detail-switch-${id}`}
|
||||
content={t('datasetDocuments.list.action.enableWarning') as string}
|
||||
className='!font-semibold'
|
||||
disabled={!archived}
|
||||
>
|
||||
<div>
|
||||
<Switch
|
||||
defaultValue={archived ? false : enabled}
|
||||
onChange={v => !archived && onOperate(v ? 'enable' : 'disable')}
|
||||
disabled={archived}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='mx-4 pb-1 pt-0.5 text-xs text-gray-500'>
|
||||
{!archived && enabled ? t('datasetDocuments.list.index.enableTip') : t('datasetDocuments.list.index.disableTip')}
|
||||
</div>
|
||||
<Divider />
|
||||
</>}
|
||||
{!archived && (
|
||||
<>
|
||||
<div className={s.actionItem} onClick={() => router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)}>
|
||||
<SettingsIcon />
|
||||
<span className={s.actionName}>{t('datasetDocuments.list.action.settings')}</span>
|
||||
</div>
|
||||
{
|
||||
!isListScene && (
|
||||
<div className={s.actionItem} onClick={showNewSegmentModal}>
|
||||
<FilePlus02 className='w-4 h-4 text-gray-500' />
|
||||
<span className={s.actionName}>{t('datasetDocuments.list.action.add')}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
data_source_type === 'notion_import' && (
|
||||
<div className={s.actionItem} onClick={() => onOperate('sync')}>
|
||||
<SyncIcon />
|
||||
<span className={s.actionName}>{t('datasetDocuments.list.action.sync')}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Divider className='my-1' />
|
||||
</>
|
||||
)}
|
||||
{!archived && <div className={s.actionItem} onClick={() => onOperate('archive')}>
|
||||
<ArchiveIcon />
|
||||
<span className={s.actionName}>{t('datasetDocuments.list.action.archive')}</span>
|
||||
</div>}
|
||||
<div
|
||||
className={cn(s.actionItem, s.deleteActionItem, 'group')}
|
||||
onClick={() => {
|
||||
setShowModal(true)
|
||||
props?.onClose()
|
||||
}}>
|
||||
<TrashIcon className={'w-4 h-4 stroke-current text-gray-500 stroke-2 group-hover:text-red-500'} />
|
||||
<span className={cn(s.actionName, 'group-hover:text-red-500')}>{t('datasetDocuments.list.action.delete')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
return <div className='flex items-center' onClick={e => e.stopPropagation()}>
|
||||
{isListScene && <>
|
||||
{archived
|
||||
? <Tooltip selector={`list-switch-${id}`} content={t('datasetDocuments.list.action.enableWarning') as string} className='!font-semibold'>
|
||||
|
@ -154,69 +218,7 @@ export const OperationAction: FC<{
|
|||
<Divider className='!ml-4 !mr-2 !h-3' type='vertical' />
|
||||
</>}
|
||||
<Popover
|
||||
htmlContent={
|
||||
<div className='w-full py-1'>
|
||||
{!isListScene && <>
|
||||
<div className='flex justify-between items-center mx-4 pt-2'>
|
||||
<span className={cn(s.actionName, 'font-medium')}>
|
||||
{!archived && enabled ? t('datasetDocuments.list.index.enable') : t('datasetDocuments.list.index.disable')}
|
||||
</span>
|
||||
<Tooltip
|
||||
selector={`detail-switch-${id}`}
|
||||
content={t('datasetDocuments.list.action.enableWarning') as string}
|
||||
className='!font-semibold'
|
||||
disabled={!archived}
|
||||
>
|
||||
<div>
|
||||
<Switch
|
||||
defaultValue={archived ? false : enabled}
|
||||
onChange={v => !archived && onOperate(v ? 'enable' : 'disable')}
|
||||
disabled={archived}
|
||||
size='md'
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='mx-4 pb-1 pt-0.5 text-xs text-gray-500'>
|
||||
{!archived && enabled ? t('datasetDocuments.list.index.enableTip') : t('datasetDocuments.list.index.disableTip')}
|
||||
</div>
|
||||
<Divider />
|
||||
</>}
|
||||
{!archived && (
|
||||
<>
|
||||
<div className={s.actionItem} onClick={() => router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)}>
|
||||
<SettingsIcon />
|
||||
<span className={s.actionName}>{t('datasetDocuments.list.action.settings')}</span>
|
||||
</div>
|
||||
{
|
||||
!isListScene && (
|
||||
<div className={s.actionItem} onClick={showNewSegmentModal}>
|
||||
<FilePlus02 className='w-4 h-4 text-gray-500' />
|
||||
<span className={s.actionName}>{t('datasetDocuments.list.action.add')}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
data_source_type === 'notion_import' && (
|
||||
<div className={s.actionItem} onClick={() => onOperate('sync')}>
|
||||
<SyncIcon />
|
||||
<span className={s.actionName}>{t('datasetDocuments.list.action.sync')}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<Divider className='my-1' />
|
||||
</>
|
||||
)}
|
||||
{!archived && <div className={s.actionItem} onClick={() => onOperate('archive')}>
|
||||
<ArchiveIcon />
|
||||
<span className={s.actionName}>{t('datasetDocuments.list.action.archive')}</span>
|
||||
</div>}
|
||||
<div className={cn(s.actionItem, s.deleteActionItem, 'group')} onClick={() => setShowModal(true)}>
|
||||
<TrashIcon className={'w-4 h-4 stroke-current text-gray-500 stroke-2 group-hover:text-red-500'} />
|
||||
<span className={cn(s.actionName, 'group-hover:text-red-500')}>{t('datasetDocuments.list.action.delete')}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
htmlContent={<Operations />}
|
||||
trigger='click'
|
||||
position='br'
|
||||
btnElement={<div className={cn(s.actionIcon, s.commonIcon)} />}
|
||||
|
|
|
@ -58,34 +58,34 @@
|
|||
}
|
||||
.actionIcon {
|
||||
@apply bg-gray-500;
|
||||
mask-image: url(./assets/action.svg);
|
||||
mask-image: url(~@/assets/action.svg);
|
||||
}
|
||||
.pdfIcon {
|
||||
background-image: url(./assets/pdf.svg);
|
||||
background-image: url(~@/assets/pdf.svg);
|
||||
}
|
||||
.jsonIcon {
|
||||
background-image: url(./assets/json.svg);
|
||||
background-image: url(~@/assets/json.svg);
|
||||
}
|
||||
.htmlIcon {
|
||||
background-image: url(./assets/html.svg);
|
||||
background-image: url(~@/assets/html.svg);
|
||||
}
|
||||
.txtIcon {
|
||||
background-image: url(./assets/txt.svg);
|
||||
background-image: url(~@/assets/txt.svg);
|
||||
}
|
||||
.markdownIcon {
|
||||
background-image: url(./assets/md.svg);
|
||||
background-image: url(~@/assets/md.svg);
|
||||
}
|
||||
.mdIcon {
|
||||
background-image: url(./assets/md.svg);
|
||||
background-image: url(~@/assets/md.svg);
|
||||
}
|
||||
.xlsIcon {
|
||||
background-image: url(./assets/xlsx.svg);
|
||||
background-image: url(~@/assets/xlsx.svg);
|
||||
}
|
||||
.xlsxIcon {
|
||||
background-image: url(./assets/xlsx.svg);
|
||||
background-image: url(~@/assets/xlsx.svg);
|
||||
}
|
||||
.csvIcon {
|
||||
background-image: url(./assets/csv.svg);
|
||||
background-image: url(~@/assets/csv.svg);
|
||||
}
|
||||
.statusItemDetail {
|
||||
@apply h-8 font-medium border border-gray-200 inline-flex items-center rounded-lg pl-3 pr-4 mr-2;
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, { useEffect, useRef, useState } from 'react'
|
|||
import copy from 'copy-to-clipboard'
|
||||
import { t } from 'i18next'
|
||||
import s from './style.module.css'
|
||||
import { randomString } from '@/app/components/app-sidebar/basic'
|
||||
import { randomString } from '@/utils'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
type IInputCopyProps = {
|
||||
|
|
|
@ -8,20 +8,22 @@ import SecretKeyModal from '@/app/components/develop/secret-key/secret-key-modal
|
|||
type ISecretKeyButtonProps = {
|
||||
className?: string
|
||||
appId: string
|
||||
iconCls?: string
|
||||
textCls?: string
|
||||
}
|
||||
|
||||
const SecretKeyButton = ({ className, appId }: ISecretKeyButtonProps) => {
|
||||
const SecretKeyButton = ({ className, appId, iconCls, textCls }: ISecretKeyButtonProps) => {
|
||||
const [isVisible, setVisible] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<Button className={`px-3 ${className}`} type='default' onClick={() => setVisible(true)}>
|
||||
<div className='flex items-center justify-center w-4 h-4 mr-2'>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<div className={'flex items-center justify-center w-4 h-4 mr-2'}>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" className={iconCls}>
|
||||
<path d="M9 3.66672C9.35362 3.66672 9.69276 3.80719 9.94281 4.05724C10.1929 4.30729 10.3333 4.64643 10.3333 5.00005M13 5.00005C13.0002 5.62483 12.854 6.24097 12.5732 6.79908C12.2924 7.3572 11.8847 7.84177 11.3829 8.21397C10.8811 8.58617 10.2991 8.83564 9.68347 8.94239C9.06788 9.04915 8.43584 9.01022 7.838 8.82872L6.33333 10.3334H5V11.6667H3.66667V13.0001H1.66667C1.48986 13.0001 1.32029 12.9298 1.19526 12.8048C1.07024 12.6798 1 12.5102 1 12.3334V10.6094C1.00004 10.4326 1.0703 10.263 1.19533 10.1381L5.17133 6.16205C5.00497 5.61206 4.95904 5.03268 5.0367 4.46335C5.11435 3.89402 5.31375 3.3481 5.62133 2.86275C5.92891 2.3774 6.33744 1.96401 6.81913 1.65073C7.30082 1.33745 7.84434 1.13162 8.41272 1.04725C8.9811 0.96289 9.56098 1.00197 10.1129 1.16184C10.6648 1.32171 11.1758 1.59861 11.6111 1.97369C12.0464 2.34878 12.3958 2.81324 12.6354 3.33548C12.8751 3.85771 12.9994 4.42545 13 5.00005Z" stroke="#1F2A37" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className='text-[13px] text-gray-800'>{t('appApi.apiKey')}</div>
|
||||
<div className={`text-[13px] text-gray-800 ${textCls}`}>{t('appApi.apiKey')}</div>
|
||||
</Button>
|
||||
<SecretKeyModal isShow={isVisible} onClose={() => setVisible(false)} appId={appId} />
|
||||
</>
|
||||
|
|
|
@ -16,11 +16,11 @@
|
|||
|
||||
.actionIcon {
|
||||
@apply bg-gray-500;
|
||||
mask-image: url(~@/app/components/datasets/documents/assets/action.svg);
|
||||
mask-image: url(~@/assets/action.svg);
|
||||
}
|
||||
|
||||
body .btn {
|
||||
background: url(~@/app/components/datasets/documents/assets/action.svg) center center no-repeat transparent;
|
||||
background: url(~@/assets/action.svg) center center no-repeat transparent;
|
||||
background-size: 16px 16px;
|
||||
/* mask-image: ; */
|
||||
}
|
||||
|
@ -32,4 +32,4 @@ body .btn:hover {
|
|||
|
||||
.deleteActionItem:hover .deleteActionItemChild {
|
||||
@apply text-red-500;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
.btn {
|
||||
background: url(~@/app/components/datasets/documents/assets/action.svg) center center no-repeat transparent;
|
||||
background: url(~@/assets/action.svg) center center no-repeat transparent;
|
||||
background-size: 16px 16px;
|
||||
/* mask-image: ; */
|
||||
}
|
||||
|
||||
.panelBorder {
|
||||
border: 0.5px solid rgba(0, 0, 0, .05);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { t } from 'i18next'
|
|||
import copy from 'copy-to-clipboard'
|
||||
import s from './index.module.css'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { randomString } from '@/app/components/app-sidebar/basic'
|
||||
import { randomString } from '@/utils'
|
||||
|
||||
type IInvitationLinkProps = {
|
||||
value?: string
|
||||
|
|
Before Width: | Height: | Size: 892 B After Width: | Height: | Size: 892 B |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 711 B After Width: | Height: | Size: 711 B |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
@ -121,7 +121,7 @@ const translation = {
|
|||
tooLong: 'Variable key: {{key}} too length. Can not be longer then 16 characters',
|
||||
notValid: 'Variable key: {{key}} is invalid. Can only contain letters, numbers, and underscores',
|
||||
notStartWithNumber: 'Variable key: {{key}} can not start with a number',
|
||||
keyAlreadyExists:'Variable key: :{{key}} already exists',
|
||||
keyAlreadyExists: 'Variable key: :{{key}} already exists',
|
||||
},
|
||||
variableConig: {
|
||||
modalTitle: 'Field settings',
|
||||
|
|
|
@ -117,7 +117,7 @@ const translation = {
|
|||
tooLong: '变量: {{key}} 长度太长。不能超过 16 个字符',
|
||||
notValid: '变量: {{key}} 非法。只能包含英文字符,数字和下划线',
|
||||
notStartWithNumber: '变量: {{key}} 不能以数字开头',
|
||||
keyAlreadyExists:'变量:{{key}} 已存在',
|
||||
keyAlreadyExists: '变量:{{key}} 已存在',
|
||||
},
|
||||
variableConig: {
|
||||
modalTitle: '变量设置',
|
||||
|
|
|
@ -33,14 +33,7 @@ const translation = {
|
|||
explanation: 'Ready-to-use AI WebApp',
|
||||
accessibleAddress: 'Public URL',
|
||||
preview: 'Preview',
|
||||
share: {
|
||||
entry: 'Share',
|
||||
explanation: 'Share the following URL to invite more people to access the application.',
|
||||
shareUrl: 'Share URL',
|
||||
linkCopied: 'Copied',
|
||||
copyLink: 'Copy Link',
|
||||
regenerate: 'Regenerate',
|
||||
},
|
||||
regenerate: 'Regenerate',
|
||||
preUseReminder: 'Please enable WebApp before continuing.',
|
||||
settings: {
|
||||
entry: 'Settings',
|
||||
|
@ -70,7 +63,7 @@ const translation = {
|
|||
},
|
||||
customize: {
|
||||
way: 'way',
|
||||
entry: 'Want to customize your WebApp?',
|
||||
entry: 'Customize',
|
||||
title: 'Customize AI WebApp',
|
||||
explanation: 'You can customize the frontend of the Web App to fit your scenario and style needs.',
|
||||
way1: {
|
||||
|
@ -93,7 +86,7 @@ const translation = {
|
|||
apiInfo: {
|
||||
title: 'Backend service API',
|
||||
explanation: 'Easily integrated into your application',
|
||||
accessibleAddress: 'API Token',
|
||||
accessibleAddress: 'Service API Endpoint',
|
||||
doc: 'API Reference',
|
||||
},
|
||||
status: {
|
||||
|
|
|
@ -33,14 +33,7 @@ const translation = {
|
|||
explanation: '开箱即用的 AI WebApp',
|
||||
accessibleAddress: '公开访问 URL',
|
||||
preview: '预览',
|
||||
share: {
|
||||
entry: '分享',
|
||||
explanation: '将以下网址分享出去,让更多人访问该应用',
|
||||
shareUrl: '分享 URL',
|
||||
copyLink: '复制链接',
|
||||
regenerate: '重新生成',
|
||||
linkCopied: '已复制',
|
||||
},
|
||||
regenerate: '重新生成',
|
||||
preUseReminder: '使用前请先打开开关',
|
||||
settings: {
|
||||
entry: '设置',
|
||||
|
@ -70,7 +63,7 @@ const translation = {
|
|||
},
|
||||
customize: {
|
||||
way: '方法',
|
||||
entry: '想要进一步自定义 WebApp?',
|
||||
entry: '定制化',
|
||||
title: '定制化 AI WebApp',
|
||||
explanation: '你可以定制化 Web App 前端以符合你的情景与风格需求',
|
||||
way1: {
|
||||
|
|
|
@ -15,6 +15,7 @@ const translation = {
|
|||
communityIntro:
|
||||
'Discuss with team members, contributors and developers on different channels.',
|
||||
roadmap: 'See our roadmap',
|
||||
appNamePlaceholder: 'Please enter the name of the app',
|
||||
newApp: {
|
||||
startToCreate: 'Let\'s start with your new app',
|
||||
captionName: 'Give your app a name',
|
||||
|
@ -36,6 +37,9 @@ const translation = {
|
|||
appCreated: 'App created',
|
||||
appCreateFailed: 'Failed to create app',
|
||||
},
|
||||
editApp: {
|
||||
startToEdit: 'Edit App',
|
||||
},
|
||||
emoji: {
|
||||
ok: 'OK',
|
||||
cancel: 'Cancel',
|
||||
|
|
|
@ -14,6 +14,7 @@ const translation = {
|
|||
join: '参与社区',
|
||||
communityIntro: '与团队成员、贡献者和开发者在不同频道中交流',
|
||||
roadmap: '产品路线图',
|
||||
appNamePlaceholder: '请输入应用名称',
|
||||
newApp: {
|
||||
startToCreate: '开始创建一个新应用',
|
||||
captionName: '给应用起个名字',
|
||||
|
@ -35,6 +36,9 @@ const translation = {
|
|||
appCreated: '应用已创建',
|
||||
appCreateFailed: '应用创建失败',
|
||||
},
|
||||
editApp: {
|
||||
startToEdit: '编辑应用',
|
||||
},
|
||||
emoji: {
|
||||
ok: '确认',
|
||||
cancel: '取消',
|
||||
|
|
|
@ -23,6 +23,8 @@ const translation = {
|
|||
lineBreak: 'Line break',
|
||||
sure: 'I\'m sure',
|
||||
download: 'Download',
|
||||
delete: 'Delete',
|
||||
settings: 'Settings',
|
||||
setup: 'Setup',
|
||||
getForFree: 'Get for free',
|
||||
reload: 'Reload',
|
||||
|
@ -35,6 +37,7 @@ const translation = {
|
|||
char: 'chars',
|
||||
},
|
||||
actionMsg: {
|
||||
noModification: 'No modifications at the moment.',
|
||||
modifiedSuccessfully: 'Modified successfully',
|
||||
modificationFailed: 'Modification failed',
|
||||
copySuccessfully: 'Copied successfully',
|
||||
|
|
|
@ -23,6 +23,8 @@ const translation = {
|
|||
lineBreak: '换行',
|
||||
sure: '我确定',
|
||||
download: '下载',
|
||||
delete: '删除',
|
||||
settings: '设置',
|
||||
setup: '设置',
|
||||
getForFree: '免费获取',
|
||||
reload: '刷新',
|
||||
|
@ -35,6 +37,7 @@ const translation = {
|
|||
char: '个字符',
|
||||
},
|
||||
actionMsg: {
|
||||
noModification: '暂无修改',
|
||||
modifiedSuccessfully: '修改成功',
|
||||
modificationFailed: '修改失败',
|
||||
copySuccessfully: '复制成功',
|
||||
|
|
|
@ -75,8 +75,6 @@ export type AppTemplatesResponse = {
|
|||
|
||||
export type CreateAppResponse = App
|
||||
|
||||
export type UpdateAppNameResponse = App
|
||||
|
||||
export type UpdateAppSiteCodeResponse = { app_id: string } & SiteConfig
|
||||
|
||||
export type AppDailyConversationsResponse = {
|
||||
|
|
|
@ -136,6 +136,7 @@ export type DataSourceInfo = {
|
|||
created_by: string
|
||||
extension: string
|
||||
}
|
||||
notion_page_icon?: string
|
||||
}
|
||||
|
||||
export type InitialDocumentDetail = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { Fetcher } from 'swr'
|
||||
import { del, get, post } from './base'
|
||||
import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, CreateApiKeyResponse, GenerationIntroductionResponse, UpdateAppModelConfigResponse, UpdateAppNameResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse } from '@/models/app'
|
||||
import type { ApikeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, CreateApiKeyResponse, GenerationIntroductionResponse, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse } from '@/models/app'
|
||||
import type { CommonResponse } from '@/models/common'
|
||||
import type { AppMode, ModelConfig } from '@/types/app'
|
||||
|
||||
|
@ -24,11 +24,6 @@ export const deleteApp: Fetcher<CommonResponse, string> = (appID) => {
|
|||
return del(`apps/${appID}`) as Promise<CommonResponse>
|
||||
}
|
||||
|
||||
// path: /apps/{appId}/name
|
||||
export const updateAppName: Fetcher<UpdateAppNameResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
|
||||
return post(url, { body }) as Promise<UpdateAppNameResponse>
|
||||
}
|
||||
|
||||
export const updateAppSiteStatus: Fetcher<AppDetailResponse, { url: string; body: Record<string, any> }> = ({ url, body }) => {
|
||||
return post(url, { body }) as Promise<AppDetailResponse>
|
||||
}
|
||||
|
|
|
@ -2,12 +2,11 @@
|
|||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import useSWR, { useSWRConfig } from 'swr'
|
||||
import { createApp, fetchAppDetail, fetchAppList, getAppDailyConversations, getAppDailyEndUsers, updateAppApiStatus, updateAppModelConfig, updateAppName, updateAppRateLimit, updateAppSiteAccessToken, updateAppSiteConfig, updateAppSiteStatus } from '../apps'
|
||||
import { createApp, fetchAppDetail, fetchAppList, getAppDailyConversations, getAppDailyEndUsers, updateAppApiStatus, updateAppModelConfig, updateAppRateLimit, updateAppSiteAccessToken, updateAppSiteConfig, updateAppSiteStatus } from '../apps'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
const Service: FC = () => {
|
||||
const { data: appList, error: appListError } = useSWR({ url: '/apps', params: { page: 1 } }, fetchAppList)
|
||||
const { data: firstApp, error: appDetailError } = useSWR({ url: '/apps', id: '1' }, fetchAppDetail)
|
||||
const { data: appName, error: appNameError } = useSWR({ url: '/apps', id: '1', body: { name: 'new name' } }, updateAppName)
|
||||
const { data: updateAppSiteStatusRes, error: err1 } = useSWR({ url: '/apps', id: '1', body: { enable_site: false } }, updateAppSiteStatus)
|
||||
const { data: updateAppApiStatusRes, error: err2 } = useSWR({ url: '/apps', id: '1', body: { enable_api: true } }, updateAppApiStatus)
|
||||
const { data: updateAppRateLimitRes, error: err3 } = useSWR({ url: '/apps', id: '1', body: { api_rpm: 10, api_rph: 20 } }, updateAppRateLimit)
|
||||
|
@ -28,10 +27,10 @@ const Service: FC = () => {
|
|||
mutate({ url: '/apps', params: { page: 1 } })
|
||||
}
|
||||
|
||||
if (appListError || appDetailError || appNameError || err1 || err2 || err3 || err4 || err5 || err6 || err7 || err8)
|
||||
return <div>{JSON.stringify(appNameError)}</div>
|
||||
if (appListError || appDetailError || err1 || err2 || err3 || err4 || err5 || err6 || err7 || err8)
|
||||
return <div>{JSON.stringify(appListError)}</div>
|
||||
|
||||
if (!appList || !firstApp || !appName || !updateAppSiteStatusRes || !updateAppApiStatusRes || !updateAppRateLimitRes || !updateAppSiteCodeRes || !updateAppSiteConfigRes || !getAppDailyConversationsRes || !getAppDailyEndUsersRes || !updateAppModelConfigRes)
|
||||
if (!appList || !firstApp || !updateAppSiteStatusRes || !updateAppApiStatusRes || !updateAppRateLimitRes || !updateAppSiteCodeRes || !updateAppSiteConfigRes || !getAppDailyConversationsRes || !getAppDailyEndUsersRes || !updateAppModelConfigRes)
|
||||
return <Loading />
|
||||
|
||||
return (
|
||||
|
@ -55,11 +54,6 @@ const Service: FC = () => {
|
|||
<button onClick={handleCreateApp}>Click me to Create App</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>3.updateAppName</div>
|
||||
<div>{JSON.stringify(appName)}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div>4.updateAppSiteStatusRes</div>
|
||||
<div>{JSON.stringify(updateAppSiteStatusRes)}</div>
|
||||
|
|
|
@ -14,11 +14,19 @@ export async function asyncRunSafe<T = any>(fn: Promise<T>): Promise<[Error] | [
|
|||
}
|
||||
|
||||
export const getTextWidthWithCanvas = (text: string, font?: string) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (ctx) {
|
||||
ctx.font = font ?? '12px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
|
||||
return Number(ctx.measureText(text).width.toFixed(2));
|
||||
ctx.font = font ?? '12px Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
|
||||
return Number(ctx.measureText(text).width.toFixed(2))
|
||||
}
|
||||
return 0;
|
||||
return 0
|
||||
}
|
||||
|
||||
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'
|
||||
|
||||
export function randomString(length: number) {
|
||||
let result = ''
|
||||
for (let i = length; i > 0; --i) result += chars[Math.floor(Math.random() * chars.length)]
|
||||
return result
|
||||
}
|
||||
|
|