Feat/e permission (#18656)

This commit is contained in:
NFish 2025-04-24 13:10:01 +08:00 committed by GitHub
parent 2259dfdc58
commit fee51ba994
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
102 changed files with 1835 additions and 657 deletions

View File

@ -20,7 +20,7 @@ import cn from '@/utils/classnames'
import { useStore } from '@/app/components/app/store' import { useStore } from '@/app/components/app/store'
import AppSideBar from '@/app/components/app-sidebar' import AppSideBar from '@/app/components/app-sidebar'
import type { NavIcon } from '@/app/components/app-sidebar/navLink' import type { NavIcon } from '@/app/components/app-sidebar/navLink'
import { fetchAppDetail, fetchAppSSO } from '@/service/apps' import { fetchAppDetail } from '@/service/apps'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
@ -32,6 +32,13 @@ export type IAppDetailLayoutProps = {
params: { appId: string } params: { appId: string }
} }
type NavigationType = {
name: string
href: string
icon: NavIcon
selectedIcon: NavIcon
}
const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => { const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const { const {
children, children,
@ -50,12 +57,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
}))) })))
const [isLoadingAppDetail, setIsLoadingAppDetail] = useState(false) const [isLoadingAppDetail, setIsLoadingAppDetail] = useState(false)
const [appDetailRes, setAppDetailRes] = useState<App | null>(null) const [appDetailRes, setAppDetailRes] = useState<App | null>(null)
const [navigation, setNavigation] = useState<Array<{ const [navigation, setNavigation] = useState<Array<NavigationType>>([])
name: string
href: string
icon: NavIcon
selectedIcon: NavIcon
}>>([])
const { systemFeatures } = useGlobalPublicStore() const { systemFeatures } = useGlobalPublicStore()
const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => { const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
@ -142,15 +144,10 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
router.replace(`/app/${appId}/configuration`) router.replace(`/app/${appId}/configuration`)
} }
else { else {
setAppDetail({ ...res, enable_sso: false }) setAppDetail({ ...res })
setNavigation(getNavigations(appId, isCurrentWorkspaceEditor, res.mode)) setNavigation(getNavigations(appId, isCurrentWorkspaceEditor, res.mode) as Array<NavigationType>)
if (systemFeatures.enable_web_sso_switch_component && canIEditApp) {
fetchAppSSO({ appId }).then((ssoRes) => {
setAppDetail({ ...res, enable_sso: ssoRes.enabled })
})
} }
} }, [appDetailRes, appId, getNavigations, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace, pathname, router, setAppDetail])
}, [appDetailRes, appId, getNavigations, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace, pathname, router, setAppDetail, systemFeatures.enable_web_sso_switch_component])
useUnmount(() => { useUnmount(() => {
setAppDetail() setAppDetail()

View File

@ -8,19 +8,16 @@ import Loading from '@/app/components/base/loading'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'
import { import {
fetchAppDetail, fetchAppDetail,
fetchAppSSO,
updateAppSSO,
updateAppSiteAccessToken, updateAppSiteAccessToken,
updateAppSiteConfig, updateAppSiteConfig,
updateAppSiteStatus, updateAppSiteStatus,
} from '@/service/apps' } from '@/service/apps'
import type { App, AppSSO } from '@/types/app' import type { App } from '@/types/app'
import type { UpdateAppSiteCodeResponse } from '@/models/app' import type { UpdateAppSiteCodeResponse } from '@/models/app'
import { asyncRunSafe } from '@/utils' import { asyncRunSafe } from '@/utils'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import type { IAppCardProps } from '@/app/components/app/overview/appCard' import type { IAppCardProps } from '@/app/components/app/overview/appCard'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type ICardViewProps = { export type ICardViewProps = {
appId: string appId: string
@ -31,19 +28,12 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
const appDetail = useAppStore(state => state.appDetail) const appDetail = useAppStore(state => state.appDetail)
const setAppDetail = useAppStore(state => state.setAppDetail) const setAppDetail = useAppStore(state => state.setAppDetail)
const { systemFeatures } = useGlobalPublicStore()
const updateAppDetail = async () => { const updateAppDetail = async () => {
try { try {
const res = await fetchAppDetail({ url: '/apps', id: appId }) const res = await fetchAppDetail({ url: '/apps', id: appId })
if (systemFeatures.enable_web_sso_switch_component) {
const ssoRes = await fetchAppSSO({ appId })
setAppDetail({ ...res, enable_sso: ssoRes.enabled })
}
else {
setAppDetail({ ...res }) setAppDetail({ ...res })
} }
}
catch (error) { console.error(error) } catch (error) { console.error(error) }
} }
@ -93,16 +83,6 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
if (!err) if (!err)
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1') localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
if (systemFeatures.enable_web_sso_switch_component) {
const [sso_err] = await asyncRunSafe<AppSSO>(
updateAppSSO({ id: appId, enabled: Boolean(params.enable_sso) }) as Promise<AppSSO>,
)
if (sso_err) {
handleCallbackResult(sso_err)
return
}
}
handleCallbackResult(err) handleCallbackResult(err)
} }

View File

@ -4,7 +4,7 @@ import { useContext, useContextSelector } from 'use-context-selector'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiMoreFill } from '@remixicon/react' import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill } from '@remixicon/react'
import s from './style.module.css' import s from './style.module.css'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import type { App } from '@/types/app' import type { App } from '@/types/app'
@ -31,6 +31,9 @@ import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-
import { fetchWorkflowDraft } from '@/service/workflow' import { fetchWorkflowDraft } from '@/service/workflow'
import { fetchInstalledAppList } from '@/service/explore' import { fetchInstalledAppList } from '@/service/explore'
import { AppTypeIcon } from '@/app/components/app/type-selector' import { AppTypeIcon } from '@/app/components/app/type-selector'
import Tooltip from '@/app/components/base/tooltip'
import AccessControl from '@/app/components/app/app-access-control'
import { AccessMode } from '@/models/access-control'
export type AppCardProps = { export type AppCardProps = {
app: App app: App
@ -53,6 +56,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
const [showDuplicateModal, setShowDuplicateModal] = useState(false) const [showDuplicateModal, setShowDuplicateModal] = useState(false)
const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false) const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
const [showConfirmDelete, setShowConfirmDelete] = useState(false) const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [showAccessControl, setShowAccessControl] = useState(false)
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([]) const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
const onConfirmDelete = useCallback(async () => { const onConfirmDelete = useCallback(async () => {
@ -71,7 +75,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
}) })
} }
setShowConfirmDelete(false) setShowConfirmDelete(false)
}, [app.id]) }, [app.id, mutateApps, notify, onPlanInfoChanged, onRefresh, t])
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({ const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
name, name,
@ -175,6 +179,13 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
setShowSwitchModal(false) setShowSwitchModal(false)
} }
const onUpdateAccessControl = useCallback(() => {
if (onRefresh)
onRefresh()
mutateApps()
setShowAccessControl(false)
}, [onRefresh, mutateApps, setShowAccessControl])
const Operations = (props: HtmlContentProps) => { const Operations = (props: HtmlContentProps) => {
const onMouseLeave = async () => { const onMouseLeave = async () => {
props.onClose?.() props.onClose?.()
@ -209,6 +220,12 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
e.preventDefault() e.preventDefault()
setShowConfirmDelete(true) setShowConfirmDelete(true)
} }
const onClickAccessControl = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
props.onClick?.()
e.preventDefault()
setShowAccessControl(true)
}
const onClickInstalledApp = async (e: React.MouseEvent<HTMLButtonElement>) => { const onClickInstalledApp = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation() e.stopPropagation()
props.onClick?.() props.onClick?.()
@ -252,6 +269,14 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
<span className={s.actionName}>{t('app.openInExplore')}</span> <span className={s.actionName}>{t('app.openInExplore')}</span>
</button> </button>
<Divider className="!my-1" /> <Divider className="!my-1" />
{
isCurrentWorkspaceEditor && <>
<button className={s.actionItem} onClick={onClickAccessControl}>
<span className={s.actionName}>{t('app.accessControl')}</span>
</button>
<Divider />
</>
}
<div <div
className={cn(s.actionItem, s.deleteActionItem, 'group')} className={cn(s.actionItem, s.deleteActionItem, 'group')}
onClick={onClickDelete} onClick={onClickDelete}
@ -278,7 +303,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
}} }}
className='relative h-[160px] group col-span-1 bg-components-card-bg border-[1px] border-solid border-components-card-border rounded-xl shadow-sm inline-flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg' className='relative h-[160px] group col-span-1 bg-components-card-bg border-[1px] border-solid border-components-card-border rounded-xl shadow-sm inline-flex flex-col transition-all duration-200 ease-in-out cursor-pointer hover:shadow-lg'
> >
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'> <div className='flex p-4 pb-3 h-[68px] items-start gap-3 grow-0 shrink-0'>
<div className='relative shrink-0'> <div className='relative shrink-0'>
<AppIcon <AppIcon
size="large" size="large"
@ -301,6 +326,17 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
{app.mode === 'completion' && <div className='truncate'>{t('app.types.completion').toUpperCase()}</div>} {app.mode === 'completion' && <div className='truncate'>{t('app.types.completion').toUpperCase()}</div>}
</div> </div>
</div> </div>
<div className='shrink-0 w-5 h-5 flex items-center justify-center'>
{app.access_mode === AccessMode.PUBLIC && <Tooltip asChild={false} popupContent={t('app.accessControlDialog.accessItems.anyone')}>
<RiGlobalLine className='text-text-accent w-4 h-4' />
</Tooltip>}
{app.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <Tooltip asChild={false} popupContent={t('app.accessControlDialog.accessItems.specific')}>
<RiLockLine className='text-text-quaternary w-4 h-4' />
</Tooltip>}
{app.access_mode === AccessMode.ORGANIZATION && <Tooltip asChild={false} popupContent={t('app.accessControlDialog.accessItems.organization')}>
<RiBuildingLine className='text-text-quaternary w-4 h-4' />
</Tooltip>}
</div>
</div> </div>
<div className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-text-tertiary'> <div className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-text-tertiary'>
<div <div
@ -357,7 +393,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
popupClassName={ popupClassName={
(app.mode === 'completion' || app.mode === 'chat') (app.mode === 'completion' || app.mode === 'chat')
? '!w-[256px] translate-x-[-224px]' ? '!w-[256px] translate-x-[-224px]'
: '!w-[160px] translate-x-[-128px]' : '!w-[216px] translate-x-[-128px]'
} }
className={'h-fit !z-20'} className={'h-fit !z-20'}
/> />
@ -418,6 +454,9 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
onClose={() => setSecretEnvList([])} onClose={() => setSecretEnvList([])}
/> />
)} )}
{showAccessControl && (
<AccessControl app={app} onConfirm={onUpdateAccessControl} onClose={() => setShowAccessControl(false)} />
)}
</> </>
) )
} }

View File

@ -1,14 +1,21 @@
'use client' 'use client'
import { useRouter, useSearchParams } from 'next/navigation' import { useRouter, useSearchParams } from 'next/navigation'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useEffect } from 'react' import React, { useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { RiDoorLockLine } from '@remixicon/react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { fetchSystemFeatures, fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share' import { fetchWebOAuth2SSOUrl, fetchWebOIDCSSOUrl, fetchWebSAMLSSOUrl } from '@/service/share'
import { setAccessToken } from '@/app/components/share/utils' import { setAccessToken } from '@/app/components/share/utils'
import Button from '@/app/components/base/button'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { SSOProtocol } from '@/types/feature'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
const WebSSOForm: FC = () => { const WebSSOForm: FC = () => {
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const searchParams = useSearchParams() const searchParams = useSearchParams()
const router = useRouter() const router = useRouter()
@ -23,15 +30,15 @@ const WebSSOForm: FC = () => {
}) })
} }
const getAppCodeFromRedirectUrl = () => { const getAppCodeFromRedirectUrl = useCallback(() => {
const appCode = redirectUrl?.split('/').pop() const appCode = redirectUrl?.split('/').pop()
if (!appCode) if (!appCode)
return null return null
return appCode return appCode
} }, [redirectUrl])
const processTokenAndRedirect = async () => { const processTokenAndRedirect = useCallback(async () => {
const appCode = getAppCodeFromRedirectUrl() const appCode = getAppCodeFromRedirectUrl()
if (!appCode || !tokenFromUrl || !redirectUrl) { if (!appCode || !tokenFromUrl || !redirectUrl) {
showErrorToast('redirect url or app code or token is invalid.') showErrorToast('redirect url or app code or token is invalid.')
@ -40,27 +47,27 @@ const WebSSOForm: FC = () => {
await setAccessToken(appCode, tokenFromUrl) await setAccessToken(appCode, tokenFromUrl)
router.push(redirectUrl) router.push(redirectUrl)
} }, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl])
const handleSSOLogin = async (protocol: string) => { const handleSSOLogin = async () => {
const appCode = getAppCodeFromRedirectUrl() const appCode = getAppCodeFromRedirectUrl()
if (!appCode || !redirectUrl) { if (!appCode || !redirectUrl) {
showErrorToast('redirect url or app code is invalid.') showErrorToast('redirect url or app code is invalid.')
return return
} }
switch (protocol) { switch (systemFeatures.webapp_auth.sso_config.protocol) {
case 'saml': { case SSOProtocol.SAML: {
const samlRes = await fetchWebSAMLSSOUrl(appCode, redirectUrl) const samlRes = await fetchWebSAMLSSOUrl(appCode, redirectUrl)
router.push(samlRes.url) router.push(samlRes.url)
break break
} }
case 'oidc': { case SSOProtocol.OIDC: {
const oidcRes = await fetchWebOIDCSSOUrl(appCode, redirectUrl) const oidcRes = await fetchWebOIDCSSOUrl(appCode, redirectUrl)
router.push(oidcRes.url) router.push(oidcRes.url)
break break
} }
case 'oauth2': { case SSOProtocol.OAuth2: {
const oauth2Res = await fetchWebOAuth2SSOUrl(appCode, redirectUrl) const oauth2Res = await fetchWebOAuth2SSOUrl(appCode, redirectUrl)
router.push(oauth2Res.url) router.push(oauth2Res.url)
break break
@ -72,32 +79,52 @@ const WebSSOForm: FC = () => {
useEffect(() => { useEffect(() => {
const init = async () => { const init = async () => {
const res = await fetchSystemFeatures()
const protocol = res.sso_enforced_for_web_protocol
if (message) { if (message) {
showErrorToast(message) showErrorToast(message)
return return
} }
if (!tokenFromUrl) { if (!tokenFromUrl)
await handleSSOLogin(protocol)
return return
}
await processTokenAndRedirect() await processTokenAndRedirect()
} }
init() init()
}, [message, tokenFromUrl]) // Added dependencies to useEffect }, [message, processTokenAndRedirect, tokenFromUrl])
if (tokenFromUrl)
return <div className='flex items-center justify-center h-full'><Loading /></div>
if (systemFeatures.webapp_auth.enabled) {
if (systemFeatures.webapp_auth.allow_sso) {
return ( return (
<div className="flex items-center justify-center h-full"> <div className="flex items-center justify-center h-full">
<div className={cn('flex flex-col items-center w-full grow justify-center', 'px-6', 'md:px-[108px]')}> <div className={cn('flex flex-col items-center w-full grow justify-center', 'px-6', 'md:px-[108px]')}>
<Loading type='area' /> <Button variant='primary' onClick={() => { handleSSOLogin() }}>{t('login.withSSO')}</Button>
</div> </div>
</div> </div>
) )
} }
return <div className="flex items-center justify-center h-full">
<div className="p-4 rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2">
<div className='flex items-center justify-center w-10 h-10 rounded-xl bg-components-card-bg shadow shadows-shadow-lg mb-2'>
<RiDoorLockLine className='w-5 h-5' />
</div>
<p className='system-sm-medium text-text-primary'>{t('login.webapp.noLoginMethod')}</p>
<p className='system-xs-regular text-text-tertiary mt-1'>{t('login.webapp.noLoginMethodTip')}</p>
</div>
<div className="relative my-2 py-2">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className='bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent h-px w-full'></div>
</div>
</div>
</div>
}
else {
return <div className="flex items-center justify-center h-full">
<p className='system-xs-regular text-text-tertiary'>{t('login.webapp.disabled')}</p>
</div>
}
}
export default React.memo(WebSSOForm) export default React.memo(WebSSOForm)

View File

@ -5,6 +5,7 @@ import { RiArrowDownSLine } from '@remixicon/react'
import React, { useCallback, useState } from 'react' import React, { useCallback, useState } from 'react'
import AppIcon from '../base/app-icon' import AppIcon from '../base/app-icon'
import SwitchAppModal from '../app/switch-app-modal' import SwitchAppModal from '../app/switch-app-modal'
import AccessControl from '../app/app-access-control'
import s from './style.module.css' import s from './style.module.css'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { import {
@ -18,7 +19,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'
import AppsContext, { useAppContext } from '@/context/app-context' import AppsContext, { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/apps' import { copyApp, deleteApp, exportAppConfig, fetchAppDetail, updateAppInfo } from '@/service/apps'
import DuplicateAppModal from '@/app/components/app/duplicate-modal' import DuplicateAppModal from '@/app/components/app/duplicate-modal'
import type { DuplicateAppModalProps } 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 CreateAppModal from '@/app/components/explore/create-app-modal'
@ -50,6 +51,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
const [showSwitchTip, setShowSwitchTip] = useState<string>('') const [showSwitchTip, setShowSwitchTip] = useState<string>('')
const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false) const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
const [showImportDSLModal, setShowImportDSLModal] = useState<boolean>(false) const [showImportDSLModal, setShowImportDSLModal] = useState<boolean>(false)
const [showAccessControl, setShowAccessControl] = useState<boolean>(false)
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([]) const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
const mutateApps = useContextSelector( const mutateApps = useContextSelector(
@ -175,7 +177,20 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
}) })
} }
setShowConfirmDelete(false) setShowConfirmDelete(false)
}, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, t]) }, [appDetail, mutateApps, notify, onPlanInfoChanged, replace, setAppDetail, t])
const handleClickAccessControl = useCallback(() => {
if (!appDetail)
return
setShowAccessControl(true)
setOpen(false)
}, [appDetail])
const handleAccessControlUpdate = useCallback(() => {
fetchAppDetail({ url: '/apps', id: appDetail!.id }).then((res) => {
setAppDetail(res)
setShowAccessControl(false)
})
}, [appDetail, setAppDetail])
const { isCurrentWorkspaceEditor } = useAppContext() const { isCurrentWorkspaceEditor } = useAppContext()
@ -374,6 +389,10 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
</div> </div>
) )
} }
<Divider />
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={handleClickAccessControl}>
<span className='text-gray-700 text-sm leading-5'>{t('app.accessControl')}</span>
</div>
<Divider className="!my-1" /> <Divider className="!my-1" />
<div className='group h-9 py-2 px-3 mx-1 flex items-center hover:bg-red-50 rounded-lg cursor-pointer' onClick={() => { <div className='group h-9 py-2 px-3 mx-1 flex items-center hover:bg-red-50 rounded-lg cursor-pointer' onClick={() => {
setOpen(false) setOpen(false)
@ -466,6 +485,11 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
onClose={() => setSecretEnvList([])} onClose={() => setSecretEnvList([])}
/> />
)} )}
{
showAccessControl && <AccessControl app={appDetail}
onConfirm={handleAccessControlUpdate}
onClose={() => { setShowAccessControl(false) }} />
}
</div> </div>
</PortalToFollowElem> </PortalToFollowElem>
) )

View File

@ -17,7 +17,7 @@ export type IAppDetailNavProps = {
desc: string desc: string
isExternal?: boolean isExternal?: boolean
icon: string icon: string
icon_background: string icon_background: string | null
navigation: Array<{ navigation: Array<{
name: string name: string
href: string href: string

View File

@ -0,0 +1,61 @@
import { Fragment, useCallback } from 'react'
import type { ReactNode } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import { RiCloseLine } from '@remixicon/react'
import cn from '@/utils/classnames'
type DialogProps = {
className?: string
children: ReactNode
show: boolean
onClose?: () => void
}
const AccessControlDialog = ({
className,
children,
show,
onClose,
}: DialogProps) => {
const close = useCallback(() => {
onClose?.()
}, [onClose])
return (
<Transition appear show={show} as={Fragment}>
<Dialog as="div" open={true} className="relative z-20" onClose={() => null}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-background-overlay bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 flex items-center justify-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className={cn('w-[600px] min-h-[323px] h-auto bg-components-panel-bg shadow-xl rounded-2xl transition-all transform relative p-0 overflow-y-auto', className)}>
<div onClick={() => close()} className="absolute top-5 right-5 w-8 h-8 flex items-center justify-center cursor-pointer z-10">
<RiCloseLine className='w-5 h-5' />
</div>
{children}
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition >
)
}
export default AccessControlDialog

View File

@ -0,0 +1,30 @@
'use client'
import type { FC, PropsWithChildren } from 'react'
import useAccessControlStore from '../../../../context/access-control-store'
import type { AccessMode } from '@/models/access-control'
type AccessControlItemProps = PropsWithChildren<{
type: AccessMode
}>
const AccessControlItem: FC<AccessControlItemProps> = ({ type, children }) => {
const { currentMenu, setCurrentMenu } = useAccessControlStore(s => ({ currentMenu: s.currentMenu, setCurrentMenu: s.setCurrentMenu }))
if (currentMenu !== type) {
return <div
className="rounded-[10px] border-[1px] cursor-pointer
border-components-option-card-option-border bg-components-option-card-option-bg
hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover"
onClick={() => setCurrentMenu(type)} >
{children}
</div>
}
return <div className="rounded-[10px] border-[1.5px]
border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-sm">
{children}
</div>
}
AccessControlItem.displayName = 'AccessControlItem'
export default AccessControlItem

View File

@ -0,0 +1,202 @@
'use client'
import { RiAddCircleFill, RiArrowRightSLine, RiOrganizationChart } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useDebounce } from 'ahooks'
import Avatar from '../../base/avatar'
import Button from '../../base/button'
import Checkbox from '../../base/checkbox'
import Input from '../../base/input'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem'
import Loading from '../../base/loading'
import useAccessControlStore from '../../../../context/access-control-store'
import classNames from '@/utils/classnames'
import { useSearchForWhiteListCandidates } from '@/service/access-control'
import type { AccessControlAccount, AccessControlGroup, Subject, SubjectAccount, SubjectGroup } from '@/models/access-control'
import { SubjectType } from '@/models/access-control'
import { useSelector } from '@/context/app-context'
export default function AddMemberOrGroupDialog() {
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [keyword, setKeyword] = useState('')
const selectedGroupsForBreadcrumb = useAccessControlStore(s => s.selectedGroupsForBreadcrumb)
const debouncedKeyword = useDebounce(keyword, { wait: 500 })
const lastAvailableGroup = selectedGroupsForBreadcrumb[selectedGroupsForBreadcrumb.length - 1]
const { isPending, isFetchingNextPage, fetchNextPage, data } = useSearchForWhiteListCandidates({ keyword: debouncedKeyword, groupId: lastAvailableGroup?.id, resultsPerPage: 10 }, open)
const handleKeywordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setKeyword(e.target.value)
}
const anchorRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const hasMore = data?.pages?.[0].hasMore ?? false
let observer: IntersectionObserver | undefined
if (anchorRef.current) {
observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !isPending && hasMore)
fetchNextPage()
}, { rootMargin: '20px' })
observer.observe(anchorRef.current)
}
return () => observer?.disconnect()
}, [isPending, fetchNextPage, anchorRef, data])
return <PortalToFollowElem open={open} onOpenChange={setOpen} offset={{ crossAxis: 300 }} placement='bottom-end'>
<PortalToFollowElemTrigger asChild>
<Button variant='ghost-accent' size='small' className='shrink-0 flex items-center gap-x-0.5' onClick={() => setOpen(!open)}>
<RiAddCircleFill className='w-4 h-4' />
<span>{t('common.operation.add')}</span>
</Button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[25]'>
<div className='w-[400px] max-h-[400px] relative overflow-y-auto flex flex-col border-[0.5px] border-components-panel-border rounded-xl bg-components-panel-bg-blur backdrop-blur-[5px] shadow-lg'>
<div className='p-2 pb-0.5 sticky top-0 bg-components-panel-bg-blur backdrop-blur-[5px] z-1'>
<Input value={keyword} onChange={handleKeywordChange} showLeftIcon placeholder={t('app.accessControlDialog.operateGroupAndMember.searchPlaceholder') as string} />
</div>
{
isPending
? <div className='p-1'><Loading /></div>
: (data?.pages?.length ?? 0) > 0
? <>
<div className='flex items-center h-7 px-2 py-0.5'>
<SelectedGroupsBreadCrumb />
</div>
<div className='p-1'>
{renderGroupOrMember(data?.pages ?? [])}
{isFetchingNextPage && <Loading />}
</div>
<div ref={anchorRef} className='h-0'> </div>
</>
: <div className='flex items-center justify-center h-7 px-2 py-0.5'>
<span className='system-xs-regular text-text-tertiary'>{t('app.accessControlDialog.operateGroupAndMember.noResult')}</span>
</div>
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
}
type GroupOrMemberData = { subjects: Subject[]; currPage: number }[]
function renderGroupOrMember(data: GroupOrMemberData) {
return data?.map((page) => {
return <div key={`search_group_member_page_${page.currPage}`}>
{page.subjects?.map((item, index) => {
if (item.subjectType === SubjectType.GROUP)
return <GroupItem key={index} group={(item as SubjectGroup).groupData} />
return <MemberItem key={index} member={(item as SubjectAccount).accountData} />
})}
</div>
}) ?? null
}
function SelectedGroupsBreadCrumb() {
const selectedGroupsForBreadcrumb = useAccessControlStore(s => s.selectedGroupsForBreadcrumb)
const setSelectedGroupsForBreadcrumb = useAccessControlStore(s => s.setSelectedGroupsForBreadcrumb)
const { t } = useTranslation()
const handleBreadCrumbClick = useCallback((index: number) => {
const newGroups = selectedGroupsForBreadcrumb.slice(0, index + 1)
setSelectedGroupsForBreadcrumb(newGroups)
}, [setSelectedGroupsForBreadcrumb, selectedGroupsForBreadcrumb])
const handleReset = useCallback(() => {
setSelectedGroupsForBreadcrumb([])
}, [setSelectedGroupsForBreadcrumb])
return <div className='flex items-center h-7 px-2 py-0.5 gap-x-0.5'>
<span className={classNames('system-xs-regular text-text-tertiary', selectedGroupsForBreadcrumb.length > 0 && 'text-text-accent cursor-pointer')} onClick={handleReset}>{t('app.accessControlDialog.operateGroupAndMember.allMembers')}</span>
{selectedGroupsForBreadcrumb.map((group, index) => {
return <div key={index} className='flex items-center gap-x-0.5 text-text-tertiary system-xs-regular'>
<span>/</span>
<span className={index === selectedGroupsForBreadcrumb.length - 1 ? '' : 'text-text-accent cursor-pointer'} onClick={() => handleBreadCrumbClick(index)}>{group.name}</span>
</div>
})}
</div>
}
type GroupItemProps = {
group: AccessControlGroup
}
function GroupItem({ group }: GroupItemProps) {
const { t } = useTranslation()
const specificGroups = useAccessControlStore(s => s.specificGroups)
const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups)
const selectedGroupsForBreadcrumb = useAccessControlStore(s => s.selectedGroupsForBreadcrumb)
const setSelectedGroupsForBreadcrumb = useAccessControlStore(s => s.setSelectedGroupsForBreadcrumb)
const isChecked = specificGroups.some(g => g.id === group.id)
const handleCheckChange = useCallback(() => {
if (!isChecked) {
const newGroups = [...specificGroups, group]
setSpecificGroups(newGroups)
}
else {
const newGroups = specificGroups.filter(g => g.id !== group.id)
setSpecificGroups(newGroups)
}
}, [specificGroups, setSpecificGroups, group, isChecked])
const handleExpandClick = useCallback(() => {
setSelectedGroupsForBreadcrumb([...selectedGroupsForBreadcrumb, group])
}, [selectedGroupsForBreadcrumb, setSelectedGroupsForBreadcrumb, group])
return <BaseItem>
<Checkbox checked={isChecked} className='w-4 h-4 shrink-0' onCheck={handleCheckChange} />
<div className='flex item-center grow'>
<div className='w-5 h-5 rounded-full bg-components-icon-bg-blue-solid overflow-hidden mr-2'>
<div className='w-full h-full flex items-center justify-center bg-access-app-icon-mask-bg'>
<RiOrganizationChart className='w-[14px] h-[14px] text-components-avatar-shape-fill-stop-0' />
</div>
</div>
<p className='system-sm-medium text-text-secondary mr-1'>{group.name}</p>
<p className='system-xs-regular text-text-tertiary'>{group.groupSize}</p>
</div>
<Button size="small" disabled={isChecked} variant='ghost-accent'
className='py-1 px-1.5 shrink-0 flex items-center justify-between' onClick={handleExpandClick}>
<span className='px-[3px]'>{t('app.accessControlDialog.operateGroupAndMember.expand')}</span>
<RiArrowRightSLine className='w-4 h-4' />
</Button>
</BaseItem>
}
type MemberItemProps = {
member: AccessControlAccount
}
function MemberItem({ member }: MemberItemProps) {
const currentUser = useSelector(s => s.userProfile)
const { t } = useTranslation()
const specificMembers = useAccessControlStore(s => s.specificMembers)
const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers)
const isChecked = specificMembers.some(m => m.id === member.id)
const handleCheckChange = useCallback(() => {
if (!isChecked) {
const newMembers = [...specificMembers, member]
setSpecificMembers(newMembers)
}
else {
const newMembers = specificMembers.filter(m => m.id !== member.id)
setSpecificMembers(newMembers)
}
}, [specificMembers, setSpecificMembers, member, isChecked])
return <BaseItem className='pr-3'>
<Checkbox checked={isChecked} className='w-4 h-4 shrink-0' onCheck={handleCheckChange} />
<div className='flex items-center grow'>
<div className='w-5 h-5 rounded-full bg-components-icon-bg-blue-solid overflow-hidden mr-2'>
<div className='w-full h-full flex items-center justify-center bg-access-app-icon-mask-bg'>
<Avatar className='w-[14px] h-[14px]' textClassName='text-[12px]' avatar={null} name={member.name} />
</div>
</div>
<p className='system-sm-medium text-text-secondary mr-1'>{member.name}</p>
{currentUser.email === member.email && <p className='system-xs-regular text-text-tertiary'>({t('common.you')})</p>}
</div>
<p className='system-xs-regular text-text-quaternary'>{member.email}</p>
</BaseItem>
}
type BaseItemProps = {
className?: string
children: React.ReactNode
}
function BaseItem({ children, className }: BaseItemProps) {
return <div className={classNames('p-1 pl-2 flex items-center space-x-2 hover:rounded-lg hover:bg-state-base-hover cursor-pointer', className)}>
{children}
</div>
}

View File

@ -0,0 +1,102 @@
'use client'
import { Dialog } from '@headlessui/react'
import { RiBuildingLine, RiGlobalLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useCallback, useEffect } from 'react'
import Button from '../../base/button'
import Toast from '../../base/toast'
import useAccessControlStore from '../../../../context/access-control-store'
import AccessControlDialog from './access-control-dialog'
import AccessControlItem from './access-control-item'
import SpecificGroupsOrMembers, { WebAppSSONotEnabledTip } from './specific-groups-or-members'
import { useGlobalPublicStore } from '@/context/global-public-context'
import type { App } from '@/types/app'
import type { Subject } from '@/models/access-control'
import { AccessMode, SubjectType } from '@/models/access-control'
import { useUpdateAccessMode } from '@/service/access-control'
type AccessControlProps = {
app: App
onClose: () => void
onConfirm?: () => void
}
export default function AccessControl(props: AccessControlProps) {
const { app, onClose, onConfirm } = props
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const setAppId = useAccessControlStore(s => s.setAppId)
const specificGroups = useAccessControlStore(s => s.specificGroups)
const specificMembers = useAccessControlStore(s => s.specificMembers)
const currentMenu = useAccessControlStore(s => s.currentMenu)
const setCurrentMenu = useAccessControlStore(s => s.setCurrentMenu)
const hideTip = systemFeatures.webapp_auth.enabled
&& (systemFeatures.webapp_auth.allow_sso
|| systemFeatures.webapp_auth.allow_email_password_login
|| systemFeatures.webapp_auth.allow_email_code_login)
useEffect(() => {
setAppId(app.id)
setCurrentMenu(app.access_mode ?? AccessMode.SPECIFIC_GROUPS_MEMBERS)
}, [app, setAppId, setCurrentMenu])
const { isPending, mutateAsync: updateAccessMode } = useUpdateAccessMode()
const handleConfirm = useCallback(async () => {
const submitData: {
appId: string
accessMode: AccessMode
subjects?: Pick<Subject, 'subjectId' | 'subjectType'>[]
} = { appId: app.id, accessMode: currentMenu }
if (currentMenu === AccessMode.SPECIFIC_GROUPS_MEMBERS) {
const subjects: Pick<Subject, 'subjectId' | 'subjectType'>[] = []
specificGroups.forEach((group) => {
subjects.push({ subjectId: group.id, subjectType: SubjectType.GROUP })
})
specificMembers.forEach((member) => {
subjects.push({
subjectId: member.id,
subjectType: SubjectType.ACCOUNT,
})
})
submitData.subjects = subjects
}
await updateAccessMode(submitData)
Toast.notify({ type: 'success', message: t('app.accessControlDialog.updateSuccess') })
onConfirm?.()
}, [updateAccessMode, app, specificGroups, specificMembers, t, onConfirm, currentMenu])
return <AccessControlDialog show onClose={onClose}>
<div className='flex flex-col gap-y-3'>
<div className='pt-6 pr-14 pb-3 pl-6'>
<Dialog.Title className='title-2xl-semi-bold text-text-primary'>{t('app.accessControlDialog.title')}</Dialog.Title>
<Dialog.Description className='mt-1 system-xs-regular text-text-tertiary'>{t('app.accessControlDialog.description')}</Dialog.Description>
</div>
<div className='px-6 pb-3 flex flex-col gap-y-1'>
<div className='leading-6'>
<p className='system-sm-medium'>{t('app.accessControlDialog.accessLabel')}</p>
</div>
<AccessControlItem type={AccessMode.ORGANIZATION}>
<div className='flex items-center p-3'>
<div className='grow flex items-center gap-x-2'>
<RiBuildingLine className='w-4 h-4 text-text-primary' />
<p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.organization')}</p>
</div>
{!hideTip && <WebAppSSONotEnabledTip />}
</div>
</AccessControlItem>
<AccessControlItem type={AccessMode.SPECIFIC_GROUPS_MEMBERS}>
<SpecificGroupsOrMembers />
</AccessControlItem>
<AccessControlItem type={AccessMode.PUBLIC}>
<div className='flex items-center p-3 gap-x-2'>
<RiGlobalLine className='w-4 h-4 text-text-primary' />
<p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.anyone')}</p>
</div>
</AccessControlItem>
</div>
<div className='flex items-center justify-end p-6 pt-5 gap-x-2'>
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button disabled={isPending} loading={isPending} variant='primary' onClick={handleConfirm}>{t('common.operation.confirm')}</Button>
</div>
</div>
</AccessControlDialog>
}

View File

@ -0,0 +1,139 @@
'use client'
import { RiAlertFill, RiCloseCircleFill, RiLockLine, RiOrganizationChart } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useCallback, useEffect } from 'react'
import Avatar from '../../base/avatar'
import Divider from '../../base/divider'
import Tooltip from '../../base/tooltip'
import Loading from '../../base/loading'
import useAccessControlStore from '../../../../context/access-control-store'
import AddMemberOrGroupDialog from './add-member-or-group-pop'
import { useGlobalPublicStore } from '@/context/global-public-context'
import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control'
import { AccessMode } from '@/models/access-control'
import { useAppWhiteListSubjects } from '@/service/access-control'
export default function SpecificGroupsOrMembers() {
const currentMenu = useAccessControlStore(s => s.currentMenu)
const appId = useAccessControlStore(s => s.appId)
const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups)
const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers)
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const hideTip = systemFeatures.webapp_auth.enabled
&& (systemFeatures.webapp_auth.allow_sso
|| systemFeatures.webapp_auth.allow_email_password_login
|| systemFeatures.webapp_auth.allow_email_code_login)
const { isPending, data } = useAppWhiteListSubjects(appId, Boolean(appId) && currentMenu === AccessMode.SPECIFIC_GROUPS_MEMBERS)
useEffect(() => {
setSpecificGroups(data?.groups ?? [])
setSpecificMembers(data?.members ?? [])
}, [data, setSpecificGroups, setSpecificMembers])
if (currentMenu !== AccessMode.SPECIFIC_GROUPS_MEMBERS) {
return <div className='flex items-center p-3'>
<div className='grow flex items-center gap-x-2'>
<RiLockLine className='w-4 h-4 text-text-primary' />
<p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.specific')}</p>
</div>
{!hideTip && <WebAppSSONotEnabledTip />}
</div>
}
return <div>
<div className='flex items-center gap-x-1 p-3'>
<div className='grow flex items-center gap-x-1'>
<RiLockLine className='w-4 h-4 text-text-primary' />
<p className='system-sm-medium text-text-primary'>{t('app.accessControlDialog.accessItems.specific')}</p>
</div>
<div className='flex items-center gap-x-1'>
{!hideTip && <>
<WebAppSSONotEnabledTip />
<Divider className='h-[14px] ml-2 mr-0' type="vertical" />
</>}
<AddMemberOrGroupDialog />
</div>
</div>
<div className='px-1 pb-1'>
<div className='bg-background-section rounded-lg p-2 flex flex-col gap-y-2 max-h-[400px] overflow-y-auto'>
{isPending ? <Loading /> : <RenderGroupsAndMembers />}
</div>
</div>
</div >
}
function RenderGroupsAndMembers() {
const { t } = useTranslation()
const specificGroups = useAccessControlStore(s => s.specificGroups)
const specificMembers = useAccessControlStore(s => s.specificMembers)
if (specificGroups.length <= 0 && specificMembers.length <= 0)
return <div className='px-2 pt-5 pb-1.5'><p className='system-xs-regular text-text-tertiary text-center'>{t('app.accessControlDialog.noGroupsOrMembers')}</p></div>
return <>
<p className='system-2xs-medium-uppercase text-text-tertiary sticky top-0'>{t('app.accessControlDialog.groups', { count: specificGroups.length ?? 0 })}</p>
<div className='flex flex-row flex-wrap gap-1'>
{specificGroups.map((group, index) => <GroupItem key={index} group={group} />)}
</div>
<p className='system-2xs-medium-uppercase text-text-tertiary sticky top-0'>{t('app.accessControlDialog.members', { count: specificMembers.length ?? 0 })}</p>
<div className='flex flex-row flex-wrap gap-1'>
{specificMembers.map((member, index) => <MemberItem key={index} member={member} />)}
</div>
</>
}
type GroupItemProps = {
group: AccessControlGroup
}
function GroupItem({ group }: GroupItemProps) {
const specificGroups = useAccessControlStore(s => s.specificGroups)
const setSpecificGroups = useAccessControlStore(s => s.setSpecificGroups)
const handleRemoveGroup = useCallback(() => {
setSpecificGroups(specificGroups.filter(g => g.id !== group.id))
}, [group, setSpecificGroups, specificGroups])
return <BaseItem icon={<RiOrganizationChart className='w-[14px] h-[14px] text-components-avatar-shape-fill-stop-0' />}
onRemove={handleRemoveGroup}>
<p className='system-xs-regular text-text-primary'>{group.name}</p>
<p className='system-xs-regular text-text-tertiary'>{group.groupSize}</p>
</BaseItem>
}
type MemberItemProps = {
member: AccessControlAccount
}
function MemberItem({ member }: MemberItemProps) {
const specificMembers = useAccessControlStore(s => s.specificMembers)
const setSpecificMembers = useAccessControlStore(s => s.setSpecificMembers)
const handleRemoveMember = useCallback(() => {
setSpecificMembers(specificMembers.filter(m => m.id !== member.id))
}, [member, setSpecificMembers, specificMembers])
return <BaseItem icon={<Avatar className='w-[14px] h-[14px]' textClassName='text-[12px]' avatar={null} name={member.name} />}
onRemove={handleRemoveMember}>
<p className='system-xs-regular text-text-primary'>{member.name}</p>
</BaseItem>
}
type BaseItemProps = {
icon: React.ReactNode
children: React.ReactNode
onRemove?: () => void
}
function BaseItem({ icon, onRemove, children }: BaseItemProps) {
return <div className='rounded-full border-[0.5px] bg-components-badge-white-to-dark shadow-xs p-1 pr-1.5 group flex items-center flex-row gap-x-1'>
<div className='w-5 h-5 rounded-full bg-components-icon-bg-blue-solid overflow-hidden'>
<div className='w-full h-full flex items-center justify-center bg-access-app-icon-mask-bg'>
{icon}
</div>
</div>
{children}
<div className='flex items-center justify-center w-4 h-4 cursor-pointer' onClick={onRemove}>
<RiCloseCircleFill className='w-[14px] h-[14px] text-text-quaternary' />
</div>
</div>
}
export function WebAppSSONotEnabledTip() {
const { t } = useTranslation()
return <Tooltip asChild={false} popupContent={t('app.accessControlDialog.webAppSSONotEnabledTip')}>
<RiAlertFill className='w-4 h-4 text-text-warning-secondary shrink-0' />
</Tooltip>
}

View File

@ -1,13 +1,18 @@
import { import {
memo, memo,
useCallback, useCallback,
useEffect,
useState, useState,
} from 'react' } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { RiArrowDownSLine, RiPlanetLine } from '@remixicon/react' import { RiArrowDownSLine, RiArrowRightSLine, RiLockLine, RiPlanetLine } from '@remixicon/react'
import Toast from '../../base/toast' import Toast from '../../base/toast'
import type { ModelAndParameter } from '../configuration/debug/types' import type { ModelAndParameter } from '../configuration/debug/types'
import Divider from '../../base/divider'
import AccessControl from '../app-access-control'
import Loading from '../../base/loading'
import Tooltip from '../../base/tooltip'
import SuggestedAction from './suggested-action' import SuggestedAction from './suggested-action'
import PublishWithMultipleModel from './publish-with-multiple-model' import PublishWithMultipleModel from './publish-with-multiple-model'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
@ -27,6 +32,9 @@ import { FileText } from '@/app/components/base/icons/src/vender/line/files'
import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button' import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/configure-button'
import type { InputVar } from '@/app/components/workflow/types' import type { InputVar } from '@/app/components/workflow/types'
import { appDefaultIconBackground } from '@/config' import { appDefaultIconBackground } from '@/config'
import { useAppWhiteListSubjects, useGetUserCanAccessApp } from '@/service/access-control'
import { AccessMode } from '@/models/access-control'
import { fetchAppDetail } from '@/service/apps'
export type AppPublisherProps = { export type AppPublisherProps = {
disabled?: boolean disabled?: boolean
@ -65,10 +73,31 @@ const AppPublisher = ({
const [published, setPublished] = useState(false) const [published, setPublished] = useState(false)
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const appDetail = useAppStore(state => state.appDetail) const appDetail = useAppStore(state => state.appDetail)
const setAppDetail = useAppStore(s => s.setAppDetail)
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {} const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
const appMode = (appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow') ? 'chat' : appDetail.mode const appMode = (appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow') ? 'chat' : appDetail.mode
const appURL = `${appBaseURL}/${appMode}/${accessToken}` const appURL = `${appBaseURL}/${appMode}/${accessToken}`
const { data: useCanAccessApp, isLoading: isGettingUserCanAccessApp, refetch } = useGetUserCanAccessApp({ appId: appDetail?.id, enabled: false })
const { data: appAccessSubjects, isLoading: isGettingAppWhiteListSubjects } = useAppWhiteListSubjects(appDetail?.id, open && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS)
useEffect(() => {
if (open && appDetail)
refetch()
}, [open, appDetail, refetch])
const [showAppAccessControl, setShowAppAccessControl] = useState(false)
const [isAppAccessSet, setIsAppAccessSet] = useState(true)
useEffect(() => {
if (appDetail && appAccessSubjects) {
if (appDetail.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && appAccessSubjects.groups?.length === 0 && appAccessSubjects.members?.length === 0)
setIsAppAccessSet(false)
else
setIsAppAccessSet(true)
}
else {
setIsAppAccessSet(true)
}
}, [appAccessSubjects, appDetail])
const language = useGetLanguage() const language = useGetLanguage()
const formatTimeFromNow = useCallback((time: number) => { const formatTimeFromNow = useCallback((time: number) => {
return dayjs(time).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow() return dayjs(time).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow()
@ -120,6 +149,13 @@ const AppPublisher = ({
} }
}, [appDetail?.id]) }, [appDetail?.id])
const handleAccessControlUpdate = useCallback(() => {
fetchAppDetail({ url: '/apps', id: appDetail!.id }).then((res) => {
setAppDetail(res)
setShowAppAccessControl(false)
})
}, [appDetail, setAppDetail])
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false) const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
return ( return (
@ -196,10 +232,37 @@ const AppPublisher = ({
) )
} }
</div> </div>
<div className='p-4 pt-3 border-t-[0.5px] border-t-black/5'> {(isGettingUserCanAccessApp || isGettingAppWhiteListSubjects)
<SuggestedAction disabled={!publishedAt} link={appURL} icon={<PlayCircle />}>{t('workflow.common.runApp')}</SuggestedAction> ? <div className='py-2'><Loading /></div>
: <>
<Divider className='my-0' />
<div className='p-4 pt-3'>
<div className='flex items-center h-6'>
<p className='system-xs-medium text-text-tertiary'>{t('app.publishApp.title')}</p>
</div>
<div className='h-8 flex items-center pl-2.5 pr-2 py-1 gap-x-0.5 rounded-lg bg-components-input-bg-normal hover:bg-primary-50 hover:text-text-accent cursor-pointer'
onClick={() => {
setShowAppAccessControl(true)
}}>
<div className='grow flex items-center gap-x-1.5 pr-1'>
<RiLockLine className='w-4 h-4 text-text-secondary shrink-0' />
{appDetail?.access_mode === AccessMode.ORGANIZATION && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.organization')}</p>}
{appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.specific')}</p>}
{appDetail?.access_mode === AccessMode.PUBLIC && <p className='system-sm-medium text-text-secondary'>{t('app.accessControlDialog.accessItems.anyone')}</p>}
</div>
{!isAppAccessSet && <p className='shrink-0 system-xs-regular text-text-tertiary'>{t('app.publishApp.notSet')}</p>}
<div className='shrink-0 w-4 h-4 flex items-center justify-center'>
<RiArrowRightSLine className='w-4 h-4 text-text-quaternary' />
</div>
</div>
{!isAppAccessSet && <p className='system-xs-regular text-text-warning mt-1'>{t('app.publishApp.notSetDesc')}</p>}
</div>
<div className='p-4 pt-3 border-t-[0.5px] border-t-black/5 flex flex-col gap-y-1'>
<Tooltip triggerClassName='flex' disabled={useCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}>
<SuggestedAction disabled={!publishedAt || !useCanAccessApp?.result} link={appURL} icon={<PlayCircle />}>{t('workflow.common.runApp')}</SuggestedAction>
</Tooltip>
{appDetail?.mode === 'workflow' {appDetail?.mode === 'workflow'
? ( ? (<div className='flex'>
<SuggestedAction <SuggestedAction
disabled={!publishedAt} disabled={!publishedAt}
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`} link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
@ -207,8 +270,9 @@ const AppPublisher = ({
> >
{t('workflow.common.batchRunApp')} {t('workflow.common.batchRunApp')}
</SuggestedAction> </SuggestedAction>
</div>
) )
: ( : (<div className='flex'>
<SuggestedAction <SuggestedAction
onClick={() => { onClick={() => {
setEmbeddingModalOpen(true) setEmbeddingModalOpen(true)
@ -219,18 +283,25 @@ const AppPublisher = ({
> >
{t('workflow.common.embedIntoSite')} {t('workflow.common.embedIntoSite')}
</SuggestedAction> </SuggestedAction>
</div>
)} )}
<Tooltip triggerClassName='flex' disabled={useCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}>
<SuggestedAction <SuggestedAction
onClick={() => { onClick={() => {
handleOpenInExplore() handleOpenInExplore()
}} }}
disabled={!publishedAt} disabled={!publishedAt || !useCanAccessApp?.result}
icon={<RiPlanetLine className='w-4 h-4' />} icon={<RiPlanetLine className='w-4 h-4' />}
> >
{t('workflow.common.openInExplore')} {t('workflow.common.openInExplore')}
</SuggestedAction> </SuggestedAction>
</Tooltip>
<div className='flex' >
<SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction> <SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction>
</div>
{appDetail?.mode === 'workflow' && ( {appDetail?.mode === 'workflow' && (
<div className='flex' >
<WorkflowToolConfigureButton <WorkflowToolConfigureButton
disabled={!publishedAt} disabled={!publishedAt}
published={!!toolPublished} published={!!toolPublished}
@ -246,8 +317,10 @@ const AppPublisher = ({
handlePublish={handlePublish} handlePublish={handlePublish}
onRefreshData={onRefreshData} onRefreshData={onRefreshData}
/> />
</div>
)} )}
</div> </div>
</>}
</div> </div>
</PortalToFollowElemContent> </PortalToFollowElemContent>
<EmbeddedModal <EmbeddedModal
@ -257,6 +330,7 @@ const AppPublisher = ({
appBaseUrl={appBaseURL} appBaseUrl={appBaseURL}
accessToken={accessToken} accessToken={accessToken}
/> />
{showAppAccessControl && <AccessControl app={appDetail!} onConfirm={handleAccessControlUpdate} onClose={() => { setShowAppAccessControl(false) }} />}
</PortalToFollowElem > </PortalToFollowElem >
) )
} }

View File

@ -8,16 +8,23 @@ export type SuggestedActionProps = PropsWithChildren<HTMLProps<HTMLAnchorElement
disabled?: boolean disabled?: boolean
}> }>
const SuggestedAction = ({ icon, link, disabled, children, className, ...props }: SuggestedActionProps) => ( const SuggestedAction = ({ icon, link, disabled, children, className, onClick, ...props }: SuggestedActionProps) => {
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
if (disabled)
return
onClick?.(e)
}
return (
<a <a
href={disabled ? undefined : link} href={disabled ? undefined : link}
target='_blank' target='_blank'
rel='noreferrer' rel='noreferrer'
className={classNames( className={classNames(
'flex justify-start items-center gap-2 h-[34px] px-2.5 bg-gray-100 rounded-lg transition-colors [&:not(:first-child)]:mt-1', 'flex-1 flex justify-start items-center text-text-secondary gap-2 h-[34px] px-2.5 bg-gray-100 rounded-lg transition-colors [&:not(:first-child)]:mt-1',
disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'hover:bg-primary-50 hover:text-primary-600 cursor-pointer', disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'hover:bg-primary-50 hover:text-primary-600 cursor-pointer',
className, className,
)} )}
onClick={handleClick}
{...props} {...props}
> >
<div className='relative w-4 h-4'>{icon}</div> <div className='relative w-4 h-4'>{icon}</div>
@ -25,5 +32,6 @@ const SuggestedAction = ({ icon, link, disabled, children, className, ...props }
<ArrowUpRight /> <ArrowUpRight />
</a> </a>
) )
}
export default SuggestedAction export default SuggestedAction

View File

@ -21,14 +21,12 @@ import type { AppIconType, AppSSO, Language } from '@/types/app'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
import { LanguagesSupported, languages } from '@/i18n/language' import { LanguagesSupported, languages } from '@/i18n/language'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context' import { useModalContext } from '@/context/modal-context'
import type { AppIconSelection } from '@/app/components/base/app-icon-picker' import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import AppIconPicker from '@/app/components/base/app-icon-picker' import AppIconPicker from '@/app/components/base/app-icon-picker'
import I18n from '@/context/i18n' import I18n from '@/context/i18n'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type ISettingsModalProps = { export type ISettingsModalProps = {
isChat: boolean isChat: boolean
@ -66,8 +64,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
onClose, onClose,
onSave, onSave,
}) => { }) => {
const { systemFeatures } = useGlobalPublicStore()
const { isCurrentWorkspaceEditor } = useAppContext()
const { notify } = useToastContext() const { notify } = useToastContext()
const [isShowMore, setIsShowMore] = useState(false) const [isShowMore, setIsShowMore] = useState(false)
const { const {
@ -139,7 +135,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
setAppIcon(icon_type === 'image' setAppIcon(icon_type === 'image'
? { type: 'image', url: icon_url!, fileId: icon } ? { type: 'image', url: icon_url!, fileId: icon }
: { type: 'emoji', icon, background: icon_background! }) : { type: 'emoji', icon, background: icon_background! })
}, [appInfo]) }, [appInfo, chat_color_theme, chat_color_theme_inverted, copyright, custom_disclaimer, default_language, description, icon, icon_background, icon_type, icon_url, privacy_policy, show_workflow_steps, title, use_icon_as_answer_icon])
const onHide = () => { const onHide = () => {
onClose() onClose()
@ -325,28 +321,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
</div> </div>
<p className='pb-0.5 text-text-tertiary body-xs-regular'>{t(`${prefixSettings}.workflow.showDesc`)}</p> <p className='pb-0.5 text-text-tertiary body-xs-regular'>{t(`${prefixSettings}.workflow.showDesc`)}</p>
</div> </div>
{/* SSO */}
{systemFeatures.enable_web_sso_switch_component && (
<>
<Divider className="h-px my-0" />
<div className='w-full'>
<p className='mb-1 system-xs-medium-uppercase text-text-tertiary'>{t(`${prefixSettings}.sso.label`)}</p>
<div className='flex justify-between items-center'>
<div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.sso.title`)}</div>
<Tooltip
disabled={systemFeatures.sso_enforced_for_web}
popupContent={
<div className='w-[180px]'>{t(`${prefixSettings}.sso.tooltip`)}</div>
}
asChild={false}
>
<Switch disabled={!systemFeatures.sso_enforced_for_web || !isCurrentWorkspaceEditor} defaultValue={systemFeatures.sso_enforced_for_web && inputInfo.enable_sso} onChange={v => setInputInfo({ ...inputInfo, enable_sso: v })}></Switch>
</Tooltip>
</div>
<p className='pb-0.5 body-xs-regular text-text-tertiary'>{t(`${prefixSettings}.sso.description`)}</p>
</div>
</>
)}
{/* more settings switch */} {/* more settings switch */}
<Divider className="h-px my-0" /> <Divider className="h-px my-0" />
{!isShowMore && ( {!isShowMore && (

View File

@ -17,7 +17,7 @@ const AppUnavailable: FC<IAppUnavailableProps> = ({
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className='flex items-center justify-center w-screen h-screen'> <div className='flex items-center justify-center w-full h-full'>
<h1 className='mr-5 h-[50px] leading-[50px] pr-5 text-[24px] font-medium' <h1 className='mr-5 h-[50px] leading-[50px] pr-5 text-[24px] font-medium'
style={{ style={{
borderRight: '1px solid rgba(0,0,0,.3)', borderRight: '1px solid rgba(0,0,0,.3)',

View File

@ -15,12 +15,15 @@ import type {
AppMeta, AppMeta,
ConversationItem, ConversationItem,
} from '@/models/share' } from '@/models/share'
import { AccessMode } from '@/models/access-control'
export type ChatWithHistoryContextValue = { export type ChatWithHistoryContextValue = {
appInfoError?: any appInfoError?: any
appInfoLoading?: boolean appInfoLoading?: boolean
appMeta?: AppMeta appMeta?: AppMeta
appData?: AppData appData?: AppData
accessMode?: AccessMode
userCanAccess?: boolean
appParams?: ChatConfig appParams?: ChatConfig
appChatListDataLoading?: boolean appChatListDataLoading?: boolean
currentConversationId: string currentConversationId: string
@ -52,6 +55,8 @@ export type ChatWithHistoryContextValue = {
} }
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
userCanAccess: false,
currentConversationId: '', currentConversationId: '',
appPrevChatTree: [], appPrevChatTree: [],
pinnedConversationList: [], pinnedConversationList: [],

View File

@ -42,6 +42,7 @@ import { changeLanguage } from '@/i18n/i18next-config'
import { useAppFavicon } from '@/hooks/use-app-favicon' import { useAppFavicon } from '@/hooks/use-app-favicon'
import { InputVarType } from '@/app/components/workflow/types' import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control'
function getFormattedChatList(messages: any[]) { function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = [] const newChatList: ChatItem[] = []
@ -72,6 +73,8 @@ function getFormattedChatList(messages: any[]) {
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo]) const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo) const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo)
const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({ appId: installedAppInfo?.app.id || appInfo?.app_id, isInstalledApp })
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ appId: installedAppInfo?.app.id || appInfo?.app_id, isInstalledApp })
useAppFavicon({ useAppFavicon({
enable: !installedAppInfo, enable: !installedAppInfo,
@ -418,7 +421,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
return { return {
appInfoError, appInfoError,
appInfoLoading, appInfoLoading: appInfoLoading || isGettingAccessMode || isCheckingPermission,
accessMode: appAccessMode?.accessMode,
userCanAccess: userCanAccessResult?.result,
isInstalledApp, isInstalledApp,
appId, appId,
currentConversationId, currentConversationId,

View File

@ -27,6 +27,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
className, className,
}) => { }) => {
const { const {
userCanAccess,
appInfoError, appInfoError,
appData, appData,
appInfoLoading, appInfoLoading,
@ -57,6 +58,8 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
<Loading type='app' /> <Loading type='app' />
) )
} }
if (!userCanAccess)
return <AppUnavailable code={403} unknownReason='no permission.' />
if (appInfoError) { if (appInfoError) {
return ( return (
@ -114,6 +117,8 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
const { const {
appInfoError, appInfoError,
appInfoLoading, appInfoLoading,
accessMode,
userCanAccess,
appData, appData,
appParams, appParams,
appMeta, appMeta,
@ -149,6 +154,8 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
appInfoError, appInfoError,
appInfoLoading, appInfoLoading,
appData, appData,
accessMode,
userCanAccess,
appParams, appParams,
appMeta, appMeta,
appChatListDataLoading, appChatListDataLoading,

View File

@ -11,10 +11,14 @@ import { Edit05 } from '@/app/components/base/icons/src/vender/line/general'
import type { ConversationItem } from '@/models/share' import type { ConversationItem } from '@/models/share'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal' import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
import MenuDropdown from '@/app/components/share/text-generation/menu-dropdown'
import { AccessMode } from '@/models/access-control'
const Sidebar = () => { const Sidebar = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
isInstalledApp,
accessMode,
appData, appData,
pinnedConversationList, pinnedConversationList,
conversationList, conversationList,
@ -115,11 +119,14 @@ const Sidebar = () => {
) )
} }
</div> </div>
<div className='flex items-center justify-between px-4 pb-4 '>
<MenuDropdown hideLogout={isInstalledApp || accessMode === AccessMode.PUBLIC} placement='top-start' data={appData?.site} />
{appData?.site.copyright && ( {appData?.site.copyright && (
<div className='px-4 pb-4 text-xs text-gray-400'> <div className='text-xs text-gray-400 truncate'>
© {(new Date()).getFullYear()} {appData?.site.copyright} © {(new Date()).getFullYear()} {appData?.site.copyright}
</div> </div>
)} )}
</div>
{!!showConfirm && ( {!!showConfirm && (
<Confirm <Confirm
title={t('share.chat.deleteConversation.title')} title={t('share.chat.deleteConversation.title')}

View File

@ -14,8 +14,11 @@ import type {
AppMeta, AppMeta,
ConversationItem, ConversationItem,
} from '@/models/share' } from '@/models/share'
import { AccessMode } from '@/models/access-control'
export type EmbeddedChatbotContextValue = { export type EmbeddedChatbotContextValue = {
accessMode?: AccessMode
userCanAccess?: boolean
appInfoError?: any appInfoError?: any
appInfoLoading?: boolean appInfoLoading?: boolean
appMeta?: AppMeta appMeta?: AppMeta
@ -46,6 +49,8 @@ export type EmbeddedChatbotContextValue = {
} }
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
userCanAccess: false,
accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
currentConversationId: '', currentConversationId: '',
appPrevChatList: [], appPrevChatList: [],
pinnedConversationList: [], pinnedConversationList: [],

View File

@ -35,6 +35,7 @@ import { changeLanguage } from '@/i18n/i18next-config'
import { InputVarType } from '@/app/components/workflow/types' import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils' import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control'
function getFormattedChatList(messages: any[]) { function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = [] const newChatList: ChatItem[] = []
@ -65,6 +66,8 @@ function getFormattedChatList(messages: any[]) {
export const useEmbeddedChatbot = () => { export const useEmbeddedChatbot = () => {
const isInstalledApp = false const isInstalledApp = false
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', fetchAppInfo) const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', fetchAppInfo)
const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({ appId: appInfo?.app_id, isInstalledApp })
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ appId: appInfo?.app_id, isInstalledApp })
const appData = useMemo(() => { const appData = useMemo(() => {
return appInfo return appInfo
@ -319,7 +322,9 @@ export const useEmbeddedChatbot = () => {
return { return {
appInfoError, appInfoError,
appInfoLoading, appInfoLoading: appInfoLoading || isGettingAccessMode || isCheckingPermission,
accessMode: appAccessMode?.accessMode,
userCanAccess: userCanAccessResult?.result,
isInstalledApp, isInstalledApp,
appId, appId,
currentConversationId, currentConversationId,

View File

@ -26,6 +26,7 @@ import Tooltip from '@/app/components/base/tooltip'
const Chatbot = () => { const Chatbot = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { const {
userCanAccess,
isMobile, isMobile,
appInfoError, appInfoError,
appInfoLoading, appInfoLoading,
@ -59,6 +60,9 @@ const Chatbot = () => {
) )
} }
if (!userCanAccess)
return <AppUnavailable code={403} unknownReason='no permission.' />
if (appInfoError) { if (appInfoError) {
return ( return (
<AppUnavailable /> <AppUnavailable />
@ -114,6 +118,8 @@ const EmbeddedChatbotWrapper = () => {
appInfoError, appInfoError,
appInfoLoading, appInfoLoading,
appData, appData,
accessMode,
userCanAccess,
appParams, appParams,
appMeta, appMeta,
appChatListDataLoading, appChatListDataLoading,
@ -139,6 +145,8 @@ const EmbeddedChatbotWrapper = () => {
} = useEmbeddedChatbot() } = useEmbeddedChatbot()
return <EmbeddedChatbotContext.Provider value={{ return <EmbeddedChatbotContext.Provider value={{
userCanAccess,
accessMode,
appInfoError, appInfoError,
appInfoLoading, appInfoLoading,
appData, appData,

View File

@ -90,6 +90,7 @@ const Tooltip: FC<TooltipProps> = ({
}} }}
onMouseLeave={() => triggerMethod === 'hover' && handleLeave(true)} onMouseLeave={() => triggerMethod === 'hover' && handleLeave(true)}
asChild={asChild} asChild={asChild}
className={!asChild ? triggerClassName : ''}
> >
{children || <div className={triggerClassName || 'p-[1px] w-3.5 h-3.5 shrink-0'}><RiQuestionLine className='text-text-quaternary hover:text-text-tertiary w-full h-full' /></div>} {children || <div className={triggerClassName || 'p-[1px] w-3.5 h-3.5 shrink-0'}><RiQuestionLine className='text-text-quaternary hover:text-text-tertiary w-full h-full' /></div>}
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>

View File

@ -18,12 +18,12 @@ import { NotionPageSelector } from '@/app/components/base/notion-page-selector'
import { useDatasetDetailContext } from '@/context/dataset-detail' import { useDatasetDetailContext } from '@/context/dataset-detail'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import VectorSpaceFull from '@/app/components/billing/vector-space-full' import VectorSpaceFull from '@/app/components/billing/vector-space-full'
import classNames from '@/utils/classnames' import { Icon3Dots } from '@/app/components/base/icons/src/vender/line/others'
type IStepOneProps = { type IStepOneProps = {
datasetId?: string datasetId?: string
dataSourceType?: DataSourceType dataSourceType?: DataSourceType
dataSourceTypeDisable: Boolean dataSourceTypeDisable: boolean
hasConnection: boolean hasConnection: boolean
onSetting: () => void onSetting: () => void
files: FileItem[] files: FileItem[]
@ -44,14 +44,20 @@ type IStepOneProps = {
type NotionConnectorProps = { type NotionConnectorProps = {
onSetting: () => void onSetting: () => void
} }
export const NotionConnector = ({ onSetting }: NotionConnectorProps) => { export const NotionConnector = (props: NotionConnectorProps) => {
const { onSetting } = props
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className={s.notionConnectionTip}> <div className='flex w-[640px] flex-col items-start rounded-2xl bg-workflow-process-bg p-6'>
<span className={s.notionIcon} /> <span className={cn(s.notionIcon, 'mb-2 h-12 w-12 rounded-[10px] border-[0.5px] border-components-card-border p-3 shadow-lg shadow-shadow-shadow-5')} />
<div className={s.title}>{t('datasetCreation.stepOne.notionSyncTitle')}</div> <div className='mb-1 flex flex-col gap-y-1 pb-3 pt-1'>
<div className={s.tip}>{t('datasetCreation.stepOne.notionSyncTip')}</div> <span className='system-md-semibold text-text-secondary'>
{t('datasetCreation.stepOne.notionSyncTitle')}
<Icon3Dots className='relative -left-1.5 -top-2.5 inline h-4 w-4 text-text-secondary' />
</span>
<div className='system-sm-regular text-text-tertiary'>{t('datasetCreation.stepOne.notionSyncTip')}</div>
</div>
<Button className='h-8' variant='primary' onClick={onSetting}>{t('datasetCreation.stepOne.connect')}</Button> <Button className='h-8' variant='primary' onClick={onSetting}>{t('datasetCreation.stepOne.connect')}</Button>
</div> </div>
) )
@ -120,27 +126,29 @@ const StepOne = ({
return true return true
if (files.some(file => !file.file.id)) if (files.some(file => !file.file.id))
return true return true
if (isShowVectorSpaceFull) return isShowVectorSpaceFull
return true
return false
}, [files, isShowVectorSpaceFull]) }, [files, isShowVectorSpaceFull])
return ( return (
<div className='flex w-full h-full'> <div className='h-full w-full overflow-x-auto'>
<div className='w-1/2 h-full overflow-y-auto relative'> <div className='flex h-full w-full min-w-[1440px]'>
<div className='relative h-full w-1/2 overflow-y-auto'>
<div className='flex justify-end'> <div className='flex justify-end'>
<div className={classNames(s.form)}> <div className={cn(s.form)}>
{ {
shouldShowDataSourceTypeList && ( shouldShowDataSourceTypeList && (
<div className={classNames(s.stepHeader, 'z-10 text-text-secondary bg-components-panel-bg-blur')}>{t('datasetCreation.steps.one')}</div> <div className={cn(s.stepHeader, 'text-text-secondary system-md-semibold')}>
{t('datasetCreation.steps.one')}
</div>
) )
} }
{ {
shouldShowDataSourceTypeList && ( shouldShowDataSourceTypeList && (
<div className='flex items-center mb-8 flex-wrap gap-4'> <div className='mb-8 grid grid-cols-3 gap-4'>
<div <div
className={cn( className={cn(
s.dataSourceItem, s.dataSourceItem,
'system-sm-medium',
dataSourceType === DataSourceType.FILE && s.active, dataSourceType === DataSourceType.FILE && s.active,
dataSourceTypeDisable && dataSourceType !== DataSourceType.FILE && s.disabled, dataSourceTypeDisable && dataSourceType !== DataSourceType.FILE && s.disabled,
)} )}
@ -153,11 +161,17 @@ const StepOne = ({
}} }}
> >
<span className={cn(s.datasetIcon)} /> <span className={cn(s.datasetIcon)} />
<span
title={t('datasetCreation.stepOne.dataSourceType.file')!}
className='truncate'
>
{t('datasetCreation.stepOne.dataSourceType.file')} {t('datasetCreation.stepOne.dataSourceType.file')}
</span>
</div> </div>
<div <div
className={cn( className={cn(
s.dataSourceItem, s.dataSourceItem,
'system-sm-medium',
dataSourceType === DataSourceType.NOTION && s.active, dataSourceType === DataSourceType.NOTION && s.active,
dataSourceTypeDisable && dataSourceType !== DataSourceType.NOTION && s.disabled, dataSourceTypeDisable && dataSourceType !== DataSourceType.NOTION && s.disabled,
)} )}
@ -170,18 +184,29 @@ const StepOne = ({
}} }}
> >
<span className={cn(s.datasetIcon, s.notion)} /> <span className={cn(s.datasetIcon, s.notion)} />
<span
title={t('datasetCreation.stepOne.dataSourceType.notion')!}
className='truncate'
>
{t('datasetCreation.stepOne.dataSourceType.notion')} {t('datasetCreation.stepOne.dataSourceType.notion')}
</span>
</div> </div>
<div <div
className={cn( className={cn(
s.dataSourceItem, s.dataSourceItem,
'system-sm-medium',
dataSourceType === DataSourceType.WEB && s.active, dataSourceType === DataSourceType.WEB && s.active,
dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled, dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled,
)} )}
onClick={() => changeType(DataSourceType.WEB)} onClick={() => changeType(DataSourceType.WEB)}
> >
<span className={cn(s.datasetIcon, s.web)} /> <span className={cn(s.datasetIcon, s.web)} />
<span
title={t('datasetCreation.stepOne.dataSourceType.web')!}
className='truncate'
>
{t('datasetCreation.stepOne.dataSourceType.web')} {t('datasetCreation.stepOne.dataSourceType.web')}
</span>
</div> </div>
</div> </div>
) )
@ -190,7 +215,7 @@ const StepOne = ({
<> <>
<FileUploader <FileUploader
fileList={files} fileList={files}
titleClassName={!shouldShowDataSourceTypeList ? 'mt-[30px] !mb-[44px] !text-lg !font-semibold !text-gray-900' : undefined} titleClassName={!shouldShowDataSourceTypeList ? 'mt-[30px] !mb-[44px] !text-lg' : undefined}
prepareFileList={updateFileList} prepareFileList={updateFileList}
onFileListUpdate={updateFileList} onFileListUpdate={updateFileList}
onFileUpdate={updateFile} onFileUpdate={updateFile}
@ -198,11 +223,11 @@ const StepOne = ({
notSupportBatchUpload={notSupportBatchUpload} notSupportBatchUpload={notSupportBatchUpload}
/> />
{isShowVectorSpaceFull && ( {isShowVectorSpaceFull && (
<div className='max-w-[640px] mb-4'> <div className='mb-4 max-w-[640px]'>
<VectorSpaceFull /> <VectorSpaceFull />
</div> </div>
)} )}
<div className="flex justify-end gap-2 max-w-[640px]"> <div className="flex max-w-[640px] justify-end gap-2">
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */} {/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
<Button disabled={nextDisabled} variant='primary' onClick={onStepChange}> <Button disabled={nextDisabled} variant='primary' onClick={onStepChange}>
<span className="flex gap-0.5 px-[10px]"> <span className="flex gap-0.5 px-[10px]">
@ -226,11 +251,11 @@ const StepOne = ({
/> />
</div> </div>
{isShowVectorSpaceFull && ( {isShowVectorSpaceFull && (
<div className='max-w-[640px] mb-4'> <div className='mb-4 max-w-[640px]'>
<VectorSpaceFull /> <VectorSpaceFull />
</div> </div>
)} )}
<div className="flex justify-end gap-2 max-w-[640px]"> <div className="flex max-w-[640px] justify-end gap-2">
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */} {/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
<Button disabled={isShowVectorSpaceFull || !notionPages.length} variant='primary' onClick={onStepChange}> <Button disabled={isShowVectorSpaceFull || !notionPages.length} variant='primary' onClick={onStepChange}>
<span className="flex gap-0.5 px-[10px]"> <span className="flex gap-0.5 px-[10px]">
@ -257,11 +282,11 @@ const StepOne = ({
/> />
</div> </div>
{isShowVectorSpaceFull && ( {isShowVectorSpaceFull && (
<div className='max-w-[640px] mb-4'> <div className='mb-4 max-w-[640px]'>
<VectorSpaceFull /> <VectorSpaceFull />
</div> </div>
)} )}
<div className="flex justify-end gap-2 max-w-[640px]"> <div className="flex max-w-[640px] justify-end gap-2">
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */} {/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
<Button disabled={isShowVectorSpaceFull || !websitePages.length} variant='primary' onClick={onStepChange}> <Button disabled={isShowVectorSpaceFull || !websitePages.length} variant='primary' onClick={onStepChange}>
<span className="flex gap-0.5 px-[10px]"> <span className="flex gap-0.5 px-[10px]">
@ -274,9 +299,9 @@ const StepOne = ({
)} )}
{!datasetId && ( {!datasetId && (
<> <>
<div className={s.dividerLine} /> <div className='my-8 h-px max-w-[640px] bg-divider-regular' />
<span className="inline-flex items-center cursor-pointer text-[13px] leading-4 text-text-accent" onClick={modalShowHandle}> <span className="inline-flex cursor-pointer items-center text-[13px] leading-4 text-text-accent" onClick={modalShowHandle}>
<RiFolder6Line className="size-4 mr-1" /> <RiFolder6Line className="mr-1 size-4" />
{t('datasetCreation.stepOne.emptyDatasetCreation')} {t('datasetCreation.stepOne.emptyDatasetCreation')}
</span> </span>
</> </>
@ -285,12 +310,13 @@ const StepOne = ({
<EmptyDatasetCreationModal show={showModal} onHide={modalCloseHandle} /> <EmptyDatasetCreationModal show={showModal} onHide={modalCloseHandle} />
</div> </div>
</div> </div>
<div className='w-1/2 h-full overflow-y-auto'> <div className='h-full w-1/2 overflow-y-auto'>
{currentFile && <FilePreview file={currentFile} hidePreview={hideFilePreview} />} {currentFile && <FilePreview file={currentFile} hidePreview={hideFilePreview} />}
{currentNotionPage && <NotionPagePreview currentPage={currentNotionPage} hidePreview={hideNotionPagePreview} />} {currentNotionPage && <NotionPagePreview currentPage={currentNotionPage} hidePreview={hideNotionPagePreview} />}
{currentWebsite && <WebsitePreview payload={currentWebsite} hidePreview={hideWebsitePreview} />} {currentWebsite && <WebsitePreview payload={currentWebsite} hidePreview={hideWebsitePreview} />}
</div> </div>
</div> </div>
</div>
) )
} }

View File

@ -26,7 +26,7 @@ const InstalledApp: FC<IInstalledAppProps> = ({
} }
return ( return (
<div className='h-full py-2 pl-0 pr-2 sm:p-2'> <div className='h-full py-2 pl-0 pr-2 sm:p-2 bg-background-default'>
{installedApp.app.mode !== 'completion' && installedApp.app.mode !== 'workflow' && ( {installedApp.app.mode !== 'completion' && installedApp.app.mode !== 'workflow' && (
<ChatWithHistory installedAppInfo={installedApp} className='rounded-2xl shadow-md overflow-hidden' /> <ChatWithHistory installedAppInfo={installedApp} className='rounded-2xl shadow-md overflow-hidden' />
)} )}

View File

@ -11,9 +11,11 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import TabHeader from '../../base/tab-header' import TabHeader from '../../base/tab-header'
import Button from '../../base/button' import Button from '../../base/button'
import { checkOrSetAccessToken } from '../utils' import { checkOrSetAccessToken } from '../utils'
import AppUnavailable from '../../base/app-unavailable'
import s from './style.module.css' import s from './style.module.css'
import RunBatch from './run-batch' import RunBatch from './run-batch'
import ResDownload from './run-batch/res-download' import ResDownload from './run-batch/res-download'
import MenuDropdown from './menu-dropdown'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import RunOnce from '@/app/components/share/text-generation/run-once' import RunOnce from '@/app/components/share/text-generation/run-once'
@ -37,6 +39,8 @@ import Toast from '@/app/components/base/toast'
import type { VisionFile, VisionSettings } from '@/types/app' import type { VisionFile, VisionSettings } from '@/types/app'
import { Resolution, TransferMethod } from '@/types/app' import { Resolution, TransferMethod } from '@/types/app'
import { useAppFavicon } from '@/hooks/use-app-favicon' import { useAppFavicon } from '@/hooks/use-app-favicon'
import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control'
import { AccessMode } from '@/models/access-control'
const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group. const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group.
enum TaskStatus { enum TaskStatus {
@ -106,6 +110,9 @@ const TextGeneration: FC<IMainProps> = ({
const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | null>(null) const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | null>(null)
const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null) const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null)
const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({ appId, isInstalledApp })
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({ appId, isInstalledApp })
// save message // save message
const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([]) const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([])
const fetchSavedMessage = async () => { const fetchSavedMessage = async () => {
@ -537,12 +544,14 @@ const TextGeneration: FC<IMainProps> = ({
</div> </div>
) )
if (!appId || !siteInfo || !promptConfig) { if (!appId || !siteInfo || !promptConfig || isGettingAccessMode || isCheckingPermission) {
return ( return (
<div className='flex items-center h-screen'> <div className='flex items-center h-screen'>
<Loading type='app' /> <Loading type='app' />
</div>) </div>)
} }
if (!userCanAccessResult?.result)
return <AppUnavailable code={403} unknownReason='no permission.' />
return ( return (
<> <>
@ -558,8 +567,9 @@ const TextGeneration: FC<IMainProps> = ({
'shrink-0 relative flex flex-col pb-10 h-full border-r border-gray-100 bg-white', 'shrink-0 relative flex flex-col pb-10 h-full border-r border-gray-100 bg-white',
)}> )}>
<div className='mb-6'> <div className='mb-6'>
<div className='flex items-center justify-between'> <div className='flex items-center'>
<div className='flex items-center space-x-3'> <div className='flex grow'>
<div className='flex items-center space-x-3 grow'>
<AppIcon <AppIcon
size="small" size="small"
iconType={siteInfo.icon_type} iconType={siteInfo.icon_type}
@ -569,6 +579,8 @@ const TextGeneration: FC<IMainProps> = ({
/> />
<div className='text-lg font-semibold text-gray-800'>{siteInfo.title}</div> <div className='text-lg font-semibold text-gray-800'>{siteInfo.title}</div>
</div> </div>
<MenuDropdown hideLogout={isInstalledApp || appAccessMode?.accessMode === AccessMode.PUBLIC} />
</div>
{!isPC && ( {!isPC && (
<Button <Button
className='shrink-0 ml-2' className='shrink-0 ml-2'

View File

@ -0,0 +1,49 @@
import React from 'react'
import cn from 'classnames'
import Modal from '@/app/components/base/modal'
import AppIcon from '@/app/components/base/app-icon'
import type { SiteInfo } from '@/models/share'
import { appDefaultIconBackground } from '@/config'
type Props = {
data?: SiteInfo
isShow: boolean
onClose: () => void
}
const InfoModal = ({
isShow,
onClose,
data,
}: Props) => {
return (
<Modal
isShow={isShow}
onClose={onClose}
className='min-w-[400px] max-w-[400px] !p-0'
closable
>
<div className={cn('flex flex-col items-center gap-4 px-4 pb-8 pt-10')}>
<AppIcon
size='xxl'
iconType={data?.icon_type}
icon={data?.icon}
background={data?.icon_background || appDefaultIconBackground}
imageUrl={data?.icon_url}
/>
<div className='system-xl-semibold text-text-secondary'>{data?.title}</div>
<div className='system-xs-regular text-text-tertiary'>
{/* copyright */}
{data?.copyright && (
<div>© {(new Date()).getFullYear()} {data?.copyright}</div>
)}
{data?.custom_disclaimer && (
<div className='mt-2'>{data.custom_disclaimer}</div>
)}
</div>
</div>
</Modal>
)
}
export default InfoModal

View File

@ -0,0 +1,113 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { Placement } from '@floating-ui/react'
import {
RiEqualizer2Line,
} from '@remixicon/react'
import { useRouter } from 'next/navigation'
import Divider from '../../base/divider'
import { removeAccessToken } from '../utils'
import InfoModal from './info-modal'
import ActionButton from '@/app/components/base/action-button'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import type { SiteInfo } from '@/models/share'
import cn from '@/utils/classnames'
type Props = {
data?: SiteInfo
placement?: Placement
hideLogout?: boolean
}
const MenuDropdown: FC<Props> = ({
data,
placement,
hideLogout,
}) => {
const router = useRouter()
const { t } = useTranslation()
const [open, doSetOpen] = useState(false)
const openRef = useRef(open)
const setOpen = useCallback((v: boolean) => {
doSetOpen(v)
openRef.current = v
}, [doSetOpen])
const handleTrigger = useCallback(() => {
setOpen(!openRef.current)
}, [setOpen])
const handleLogout = useCallback(() => {
removeAccessToken()
router.replace(`/webapp-signin?redirect_url=${window.location.href}`)
}, [router])
const [show, setShow] = useState(false)
return (
<>
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement={placement || 'bottom-end'}
offset={{
mainAxis: 4,
crossAxis: -4,
}}
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
<div>
<ActionButton size='l' className={cn(open && 'bg-state-base-hover')}>
<RiEqualizer2Line className='h-[18px] w-[18px]' />
</ActionButton>
</div>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'>
<div className='w-[224px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm'>
<div className='p-1'>
{data?.privacy_policy && (
<a href={data.privacy_policy} target='_blank' className='system-md-regular flex cursor-pointer items-center rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>
<span className='grow'>{t('share.chat.privacyPolicyMiddle')}</span>
</a>
)}
<div
onClick={() => {
handleTrigger()
setShow(true)
}}
className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'
>{t('common.userProfile.about')}</div>
{!hideLogout && (
<>
<Divider />
<div
onClick={() => {
handleLogout()
}}
className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-destructive hover:bg-state-base-hover'
>{t('common.userProfile.logout')}</div>
</>
)}
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
{show && (
<InfoModal
isShow={show}
onClose={() => {
setShow(false)
}}
data={data}
/>
)}
</>
)
}
export default React.memo(MenuDropdown)

View File

@ -0,0 +1,34 @@
import { create } from 'zustand'
import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control'
import { AccessMode } from '@/models/access-control'
import type { App } from '@/types/app'
type AccessControlStore = {
appId: App['id']
setAppId: (appId: App['id']) => void
specificGroups: AccessControlGroup[]
setSpecificGroups: (specificGroups: AccessControlGroup[]) => void
specificMembers: AccessControlAccount[]
setSpecificMembers: (specificMembers: AccessControlAccount[]) => void
currentMenu: AccessMode
setCurrentMenu: (currentMenu: AccessMode) => void
selectedGroupsForBreadcrumb: AccessControlGroup[]
setSelectedGroupsForBreadcrumb: (selectedGroupsForBreadcrumb: AccessControlGroup[]) => void
}
const useAccessControlStore = create<AccessControlStore>((set) => {
return {
appId: '',
setAppId: appId => set({ appId }),
specificGroups: [],
setSpecificGroups: specificGroups => set({ specificGroups }),
specificMembers: [],
setSpecificMembers: specificMembers => set({ specificMembers }),
currentMenu: AccessMode.SPECIFIC_GROUPS_MEMBERS,
setCurrentMenu: currentMenu => set({ currentMenu }),
selectedGroupsForBreadcrumb: [],
setSelectedGroupsForBreadcrumb: selectedGroupsForBreadcrumb => set({ selectedGroupsForBreadcrumb }),
}
})
export default useAccessControlStore

View File

@ -9,6 +9,7 @@ export default function useDocumentTitle(title: string) {
if (systemFeatures.branding.enabled) { if (systemFeatures.branding.enabled) {
document.title = `${prefix}${systemFeatures.branding.application_title}` document.title = `${prefix}${systemFeatures.branding.application_title}`
const faviconEle = document.querySelector('link[rel*=\'icon\']') as HTMLLinkElement const faviconEle = document.querySelector('link[rel*=\'icon\']') as HTMLLinkElement
if (faviconEle)
faviconEle.href = systemFeatures.branding.favicon faviconEle.href = systemFeatures.branding.favicon
} }
else { else {

View File

@ -30,26 +30,26 @@ const translation = {
overview: { overview: {
title: 'Übersicht', title: 'Übersicht',
appInfo: { appInfo: {
explanation: 'Einsatzbereite AI-WebApp', explanation: 'Einsatzbereite AI-web app',
accessibleAddress: 'Öffentliche URL', accessibleAddress: 'Öffentliche URL',
preview: 'Vorschau', preview: 'Vorschau',
regenerate: 'Regenerieren', regenerate: 'Regenerieren',
regenerateNotice: 'Möchten Sie die öffentliche URL neu generieren?', regenerateNotice: 'Möchten Sie die öffentliche URL neu generieren?',
preUseReminder: 'Bitte aktivieren Sie WebApp, bevor Sie fortfahren.', preUseReminder: 'Bitte aktivieren Sie web app, bevor Sie fortfahren.',
settings: { settings: {
entry: 'Einstellungen', entry: 'Einstellungen',
title: 'WebApp-Einstellungen', title: 'web app Einstellungen',
webName: 'WebApp-Name', webName: 'web app Name',
webDesc: 'WebApp-Beschreibung', webDesc: 'web app Beschreibung',
webDescTip: 'Dieser Text wird auf der Clientseite angezeigt und bietet grundlegende Anleitungen zur Verwendung der Anwendung', webDescTip: 'Dieser Text wird auf der Clientseite angezeigt und bietet grundlegende Anleitungen zur Verwendung der Anwendung',
webDescPlaceholder: 'Geben Sie die Beschreibung der WebApp ein', webDescPlaceholder: 'Geben Sie die Beschreibung der web app ein',
language: 'Sprache', language: 'Sprache',
workflow: { workflow: {
title: 'Workflow-Schritte', title: 'Workflow-Schritte',
show: 'Anzeigen', show: 'Anzeigen',
hide: 'Verbergen', hide: 'Verbergen',
subTitle: 'Details zum Arbeitsablauf', subTitle: 'Details zum Arbeitsablauf',
showDesc: 'Ein- oder Ausblenden von Workflow-Details in der WebApp', showDesc: 'Ein- oder Ausblenden von Workflow-Details in der web app',
}, },
chatColorTheme: 'Chat-Farbschema', chatColorTheme: 'Chat-Farbschema',
chatColorThemeDesc: 'Legen Sie das Farbschema des Chatbots fest', chatColorThemeDesc: 'Legen Sie das Farbschema des Chatbots fest',
@ -69,10 +69,10 @@ const translation = {
copyrightTooltip: 'Bitte führen Sie ein Upgrade auf den Professional-Plan oder höher durch', copyrightTooltip: 'Bitte führen Sie ein Upgrade auf den Professional-Plan oder höher durch',
}, },
sso: { sso: {
title: 'WebApp-SSO', title: 'web app SSO',
description: 'Alle Benutzer müssen sich mit SSO anmelden, bevor sie WebApp verwenden können', description: 'Alle Benutzer müssen sich mit SSO anmelden, bevor sie web app verwenden können',
label: 'SSO-Authentifizierung', label: 'SSO-Authentifizierung',
tooltip: 'Wenden Sie sich an den Administrator, um WebApp-SSO zu aktivieren', tooltip: 'Wenden Sie sich an den Administrator, um web app SSO zu aktivieren',
}, },
modalTip: 'Einstellungen für clientseitige Web-Apps.', modalTip: 'Einstellungen für clientseitige Web-Apps.',
}, },
@ -94,7 +94,7 @@ const translation = {
customize: { customize: {
way: 'Art', way: 'Art',
entry: 'Anpassen', entry: 'Anpassen',
title: 'AI-WebApp anpassen', title: 'AI-web app anpassen',
explanation: 'Sie können das Frontend der Web-App an Ihre Szenarien und Stilbedürfnisse anpassen.', explanation: 'Sie können das Frontend der Web-App an Ihre Szenarien und Stilbedürfnisse anpassen.',
way1: { way1: {
name: 'Forken Sie den Client-Code, ändern Sie ihn und deployen Sie ihn auf Vercel (empfohlen)', name: 'Forken Sie den Client-Code, ändern Sie ihn und deployen Sie ihn auf Vercel (empfohlen)',

View File

@ -163,9 +163,9 @@ const translation = {
}, },
}, },
answerIcon: { answerIcon: {
descriptionInExplore: 'Gibt an, ob das WebApp-Symbol zum Ersetzen 🤖 in Explore verwendet werden soll', descriptionInExplore: 'Gibt an, ob das web app Symbol zum Ersetzen 🤖 in Explore verwendet werden soll',
title: 'Verwenden Sie das WebApp-Symbol, um es zu ersetzen 🤖', title: 'Verwenden Sie das web app Symbol, um es zu ersetzen 🤖',
description: 'Gibt an, ob das WebApp-Symbol zum Ersetzen 🤖 in der freigegebenen Anwendung verwendet werden soll', description: 'Gibt an, ob das web app Symbol zum Ersetzen 🤖 in der freigegebenen Anwendung verwendet werden soll',
}, },
importFromDSLUrlPlaceholder: 'DSL-Link hier einfügen', importFromDSLUrlPlaceholder: 'DSL-Link hier einfügen',
duplicate: 'Duplikat', duplicate: 'Duplikat',

View File

@ -5,7 +5,7 @@ const translation = {
suffix: 'um deine Marke anzupassen.', suffix: 'um deine Marke anzupassen.',
}, },
webapp: { webapp: {
title: 'WebApp Marke anpassen', title: 'web app Marke anpassen',
removeBrand: 'Entferne Powered by Dify', removeBrand: 'Entferne Powered by Dify',
changeLogo: 'Ändere Powered by Markenbild', changeLogo: 'Ändere Powered by Markenbild',
changeLogoTip: 'SVG oder PNG Format mit einer Mindestgröße von 40x40px', changeLogoTip: 'SVG oder PNG Format mit einer Mindestgröße von 40x40px',

View File

@ -30,27 +30,27 @@ const translation = {
overview: { overview: {
title: 'Overview', title: 'Overview',
appInfo: { appInfo: {
explanation: 'Ready-to-use AI WebApp', explanation: 'Ready-to-use AI web app',
accessibleAddress: 'Public URL', accessibleAddress: 'Public URL',
preview: 'Preview', preview: 'Preview',
regenerate: 'Regenerate', regenerate: 'Regenerate',
regenerateNotice: 'Do you want to regenerate the public URL?', regenerateNotice: 'Do you want to regenerate the public URL?',
preUseReminder: 'Please enable WebApp before continuing.', preUseReminder: 'Please enable web app before continuing.',
settings: { settings: {
entry: 'Settings', entry: 'Settings',
title: 'Web App Settings', title: 'Web App Settings',
modalTip: 'Client-side web app settings. ', modalTip: 'Client-side web app settings. ',
webName: 'WebApp Name', webName: 'web app Name',
webDesc: 'WebApp Description', webDesc: 'web app Description',
webDescTip: 'This text will be displayed on the client side, providing basic guidance on how to use the application', webDescTip: 'This text will be displayed on the client side, providing basic guidance on how to use the application',
webDescPlaceholder: 'Enter the description of the WebApp', webDescPlaceholder: 'Enter the description of the web app',
language: 'Language', language: 'Language',
workflow: { workflow: {
title: 'Workflow', title: 'Workflow',
subTitle: 'Workflow Details', subTitle: 'Workflow Details',
show: 'Show', show: 'Show',
hide: 'Hide', hide: 'Hide',
showDesc: 'Show or hide workflow details in WebApp', showDesc: 'Show or hide workflow details in web app',
}, },
chatColorTheme: 'Chat color theme', chatColorTheme: 'Chat color theme',
chatColorThemeDesc: 'Set the color theme of the chatbot', chatColorThemeDesc: 'Set the color theme of the chatbot',
@ -58,9 +58,9 @@ const translation = {
invalidHexMessage: 'Invalid hex value', invalidHexMessage: 'Invalid hex value',
sso: { sso: {
label: 'SSO Enforcement', label: 'SSO Enforcement',
title: 'WebApp SSO', title: 'web app SSO',
description: 'All users are required to login with SSO before using WebApp', description: 'All users are required to login with SSO before using web app',
tooltip: 'Contact the administrator to enable WebApp SSO', tooltip: 'Contact the administrator to enable web app SSO',
}, },
more: { more: {
entry: 'Show more settings', entry: 'Show more settings',
@ -94,7 +94,7 @@ const translation = {
customize: { customize: {
way: 'way', way: 'way',
entry: 'Customize', entry: 'Customize',
title: 'Customize AI WebApp', title: 'Customize AI web app',
explanation: 'You can customize the frontend of the Web App to fit your scenario and style needs.', explanation: 'You can customize the frontend of the Web App to fit your scenario and style needs.',
way1: { way1: {
name: 'Fork the client code, modify it and deploy to Vercel (recommended)', name: 'Fork the client code, modify it and deploy to Vercel (recommended)',

View File

@ -112,9 +112,9 @@ const translation = {
image: 'Image', image: 'Image',
}, },
answerIcon: { answerIcon: {
title: 'Use WebApp icon to replace 🤖', title: 'Use web app icon to replace 🤖',
description: 'Whether to use the WebApp icon to replace 🤖 in the shared application', description: 'Whether to use the web app icon to replace 🤖 in the shared application',
descriptionInExplore: 'Whether to use the WebApp icon to replace 🤖 in Explore', descriptionInExplore: 'Whether to use the web app icon to replace 🤖 in Explore',
}, },
switch: 'Switch to Workflow Orchestrate', switch: 'Switch to Workflow Orchestrate',
switchTipStart: 'A new app copy will be created for you, and the new copy will switch to Workflow Orchestrate. The new copy will ', switchTipStart: 'A new app copy will be created for you, and the new copy will switch to Workflow Orchestrate. The new copy will ',
@ -174,6 +174,36 @@ const translation = {
}, },
}, },
showMyCreatedAppsOnly: 'Created by me', showMyCreatedAppsOnly: 'Created by me',
accessControl: 'Web App Access Control',
accessControlDialog: {
title: 'Web App Access Control',
description: 'Set web app access permissions',
accessLabel: 'Who has access',
accessItems: {
anyone: 'Anyone with the link',
specific: 'Specific groups or members',
organization: 'Only members within the enterprise',
},
groups_one: '{{count}} GROUP',
groups_other: '{{count}} GROUPS',
members_one: '{{count}} MEMBER',
members_other: '{{count}} MEMBERS',
noGroupsOrMembers: 'No groups or members selected',
webAppSSONotEnabledTip: 'Please contact enterprise administrator to configure the web app authentication method.',
operateGroupAndMember: {
searchPlaceholder: 'Search groups and members',
allMembers: 'All members',
expand: 'Expand',
noResult: 'No result',
},
updateSuccess: 'Update successfully',
},
publishApp: {
title: 'Who can access web app',
notSet: 'Not set',
notSetDesc: 'Currently nobody can access the web app. Please set permissions.',
},
noAccessPermission: 'No permission to access web app',
} }
export default translation export default translation

View File

@ -622,6 +622,7 @@ const translation = {
pagination: { pagination: {
perPage: 'Items per page', perPage: 'Items per page',
}, },
you: 'You',
} }
export default translation export default translation

View File

@ -5,7 +5,7 @@ const translation = {
suffix: 'customize your brand.', suffix: 'customize your brand.',
}, },
webapp: { webapp: {
title: 'Customize WebApp brand', title: 'Customize web app brand',
removeBrand: 'Remove Powered by Dify', removeBrand: 'Remove Powered by Dify',
changeLogo: 'Change Powered by Brand Image', changeLogo: 'Change Powered by Brand Image',
changeLogoTip: 'SVG or PNG format with a minimum size of 40x40px', changeLogoTip: 'SVG or PNG format with a minimum size of 40x40px',

View File

@ -104,6 +104,11 @@ const translation = {
licenseLostTip: 'Failed to connect Dify license server. Please contact your administrator to continue using Dify.', licenseLostTip: 'Failed to connect Dify license server. Please contact your administrator to continue using Dify.',
licenseInactive: 'License Inactive', licenseInactive: 'License Inactive',
licenseInactiveTip: 'The Dify Enterprise license for your workspace is inactive. Please contact your administrator to continue using Dify.', licenseInactiveTip: 'The Dify Enterprise license for your workspace is inactive. Please contact your administrator to continue using Dify.',
webapp: {
noLoginMethod: 'Authentication method not configured for web app',
noLoginMethodTip: 'Please contact the system admin to add an authentication method.',
disabled: 'Webapp authentication is disabled. Please contact the system admin to enable it. You can try to use the app directly.',
},
} }
export default translation export default translation

View File

@ -49,7 +49,7 @@ const translation = {
show: 'Mostrar', show: 'Mostrar',
hide: 'Ocultar', hide: 'Ocultar',
subTitle: 'Detalles del flujo de trabajo', subTitle: 'Detalles del flujo de trabajo',
showDesc: 'Mostrar u ocultar detalles del flujo de trabajo en WebApp', showDesc: 'Mostrar u ocultar detalles del flujo de trabajo en web app',
}, },
chatColorTheme: 'Tema de color del chat', chatColorTheme: 'Tema de color del chat',
chatColorThemeDesc: 'Establece el tema de color del chatbot', chatColorThemeDesc: 'Establece el tema de color del chatbot',
@ -69,10 +69,10 @@ const translation = {
copyrightTooltip: 'Actualice al plan Profesional o superior', copyrightTooltip: 'Actualice al plan Profesional o superior',
}, },
sso: { sso: {
description: 'Todos los usuarios deben iniciar sesión con SSO antes de usar WebApp', description: 'Todos los usuarios deben iniciar sesión con SSO antes de usar web app',
tooltip: 'Póngase en contacto con el administrador para habilitar el inicio de sesión único de WebApp', tooltip: 'Póngase en contacto con el administrador para habilitar el inicio de sesión único de web app',
label: 'Autenticación SSO', label: 'Autenticación SSO',
title: 'WebApp SSO', title: 'web app SSO',
}, },
modalTip: 'Configuración de la aplicación web del lado del cliente.', modalTip: 'Configuración de la aplicación web del lado del cliente.',
}, },

View File

@ -5,7 +5,7 @@ const translation = {
suffix: 'personalizar tu marca.', suffix: 'personalizar tu marca.',
}, },
webapp: { webapp: {
title: 'Personalizar marca de WebApp', title: 'Personalizar marca de web app',
removeBrand: 'Eliminar Powered by Dify', removeBrand: 'Eliminar Powered by Dify',
changeLogo: 'Cambiar Imagen de Marca Powered by', changeLogo: 'Cambiar Imagen de Marca Powered by',
changeLogoTip: 'Formato SVG o PNG con un tamaño mínimo de 40x40px', changeLogoTip: 'Formato SVG o PNG con un tamaño mínimo de 40x40px',

View File

@ -35,20 +35,20 @@ const translation = {
preview: 'پیش‌نمایش', preview: 'پیش‌نمایش',
regenerate: 'تولید مجدد', regenerate: 'تولید مجدد',
regenerateNotice: 'آیا می‌خواهید آدرس عمومی را دوباره تولید کنید؟', regenerateNotice: 'آیا می‌خواهید آدرس عمومی را دوباره تولید کنید؟',
preUseReminder: 'لطفاً قبل از ادامه، WebApp را فعال کنید.', preUseReminder: 'لطفاً قبل از ادامه، web app را فعال کنید.',
settings: { settings: {
entry: 'تنظیمات', entry: 'تنظیمات',
title: 'تنظیمات WebApp', title: 'تنظیمات web app',
webName: 'نام WebApp', webName: 'نام web app',
webDesc: 'توضیحات WebApp', webDesc: 'توضیحات web app',
webDescTip: 'این متن در سمت مشتری نمایش داده می‌شود و راهنمایی‌های اولیه در مورد نحوه استفاده از برنامه را ارائه می‌دهد', webDescTip: 'این متن در سمت مشتری نمایش داده می‌شود و راهنمایی‌های اولیه در مورد نحوه استفاده از برنامه را ارائه می‌دهد',
webDescPlaceholder: 'توضیحات WebApp را وارد کنید', webDescPlaceholder: 'توضیحات web app را وارد کنید',
language: 'زبان', language: 'زبان',
workflow: { workflow: {
title: 'مراحل کاری', title: 'مراحل کاری',
show: 'نمایش', show: 'نمایش',
hide: 'مخفی کردن', hide: 'مخفی کردن',
showDesc: 'نمایش یا پنهان کردن جزئیات گردش کار در WebApp', showDesc: 'نمایش یا پنهان کردن جزئیات گردش کار در web app',
subTitle: 'جزئیات گردش کار', subTitle: 'جزئیات گردش کار',
}, },
chatColorTheme: 'تم رنگی چت', chatColorTheme: 'تم رنگی چت',
@ -69,10 +69,10 @@ const translation = {
copyrightTooltip: 'لطفا به طرح حرفه ای یا بالاتر ارتقا دهید', copyrightTooltip: 'لطفا به طرح حرفه ای یا بالاتر ارتقا دهید',
}, },
sso: { sso: {
title: 'WebApp SSO', title: 'web app SSO',
label: 'احراز هویت SSO', label: 'احراز هویت SSO',
description: 'همه کاربران باید قبل از استفاده از WebApp با SSO وارد شوند', description: 'همه کاربران باید قبل از استفاده از web app با SSO وارد شوند',
tooltip: 'برای فعال کردن WebApp SSO با سرپرست تماس بگیرید', tooltip: 'برای فعال کردن web app SSO با سرپرست تماس بگیرید',
}, },
modalTip: 'تنظیمات برنامه وب سمت مشتری.', modalTip: 'تنظیمات برنامه وب سمت مشتری.',
}, },
@ -94,7 +94,7 @@ const translation = {
customize: { customize: {
way: 'راه', way: 'راه',
entry: 'سفارشی‌سازی', entry: 'سفارشی‌سازی',
title: 'سفارشی‌سازی WebApp AI', title: 'سفارشی‌سازی web app AI',
explanation: 'شما می‌توانید ظاهر جلویی برنامه وب را برای برآوردن نیازهای سناریو و سبک خود سفارشی کنید.', explanation: 'شما می‌توانید ظاهر جلویی برنامه وب را برای برآوردن نیازهای سناریو و سبک خود سفارشی کنید.',
way1: { way1: {
name: 'کلاینت را شاخه کنید، آن را تغییر دهید و در Vercel مستقر کنید (توصیه می‌شود)', name: 'کلاینت را شاخه کنید، آن را تغییر دهید و در Vercel مستقر کنید (توصیه می‌شود)',

View File

@ -165,9 +165,9 @@ const translation = {
}, },
}, },
answerIcon: { answerIcon: {
descriptionInExplore: 'آیا از نماد WebApp برای جایگزینی 🤖 در Explore استفاده کنیم یا خیر', descriptionInExplore: 'آیا از نماد web app برای جایگزینی 🤖 در Explore استفاده کنیم یا خیر',
description: 'آیا از نماد WebApp برای جایگزینی 🤖 در برنامه مشترک استفاده کنیم یا خیر', description: 'آیا از نماد web app برای جایگزینی 🤖 در برنامه مشترک استفاده کنیم یا خیر',
title: 'از نماد WebApp برای جایگزینی 🤖 استفاده کنید', title: 'از نماد web app برای جایگزینی 🤖 استفاده کنید',
}, },
mermaid: { mermaid: {
handDrawn: 'دست کشیده شده', handDrawn: 'دست کشیده شده',

View File

@ -30,12 +30,12 @@ const translation = {
overview: { overview: {
title: 'Aperçu', title: 'Aperçu',
appInfo: { appInfo: {
explanation: 'WebApp AI prête à l\'emploi', explanation: 'web app AI prête à l\'emploi',
accessibleAddress: 'URL publique', accessibleAddress: 'URL publique',
preview: 'Aperçu', preview: 'Aperçu',
regenerate: 'Regénérer', regenerate: 'Regénérer',
regenerateNotice: 'Voulez-vous régénérer l\'URL publique ?', regenerateNotice: 'Voulez-vous régénérer l\'URL publique ?',
preUseReminder: 'Veuillez activer WebApp avant de continuer.', preUseReminder: 'Veuillez activer web app avant de continuer.',
settings: { settings: {
entry: 'Paramètres', entry: 'Paramètres',
title: 'Paramètres de l\'application Web', title: 'Paramètres de l\'application Web',
@ -48,7 +48,7 @@ const translation = {
title: 'Étapes du workflow', title: 'Étapes du workflow',
show: 'Afficher', show: 'Afficher',
hide: 'Masquer', hide: 'Masquer',
showDesc: 'Afficher ou masquer les détails du flux de travail dans WebApp', showDesc: 'Afficher ou masquer les détails du flux de travail dans web app',
subTitle: 'Détails du flux de travail', subTitle: 'Détails du flux de travail',
}, },
chatColorTheme: 'Thème de couleur du chatbot', chatColorTheme: 'Thème de couleur du chatbot',
@ -70,9 +70,9 @@ const translation = {
}, },
sso: { sso: {
label: 'Authentification SSO', label: 'Authentification SSO',
title: 'WebApp SSO', title: 'web app SSO',
tooltip: 'Contactez ladministrateur pour activer lauthentification unique WebApp', tooltip: 'Contactez ladministrateur pour activer lauthentification unique web app',
description: 'Tous les utilisateurs doivent se connecter avec lauthentification unique avant dutiliser WebApp', description: 'Tous les utilisateurs doivent se connecter avec lauthentification unique avant dutiliser web app',
}, },
modalTip: 'Paramètres de lapplication web côté client.', modalTip: 'Paramètres de lapplication web côté client.',
}, },

View File

@ -161,9 +161,9 @@ const translation = {
}, },
}, },
answerIcon: { answerIcon: {
description: 'Sil faut utiliser licône WebApp pour remplacer 🤖 dans lapplication partagée', description: 'Sil faut utiliser licône web app pour remplacer 🤖 dans lapplication partagée',
title: 'Utiliser licône WebApp pour remplacer 🤖', title: 'Utiliser licône web app pour remplacer 🤖',
descriptionInExplore: 'Utilisation de licône WebApp pour remplacer 🤖 dans Explore', descriptionInExplore: 'Utilisation de licône web app pour remplacer 🤖 dans Explore',
}, },
importFromDSLUrlPlaceholder: 'Collez le lien DSL ici', importFromDSLUrlPlaceholder: 'Collez le lien DSL ici',
importFromDSL: 'Importation à partir dune DSL', importFromDSL: 'Importation à partir dune DSL',

View File

@ -5,7 +5,7 @@ const translation = {
suffix: 'personnalisez votre marque.', suffix: 'personnalisez votre marque.',
}, },
webapp: { webapp: {
title: 'Personnalisez la marque WebApp', title: 'Personnalisez la marque web app',
removeBrand: 'Supprimer Propulsé par Dify', removeBrand: 'Supprimer Propulsé par Dify',
changeLogo: 'Changer Propulsé par l\'Image de Marque', changeLogo: 'Changer Propulsé par l\'Image de Marque',
changeLogoTip: 'Format SVG ou PNG avec une taille minimum de 40x40px', changeLogoTip: 'Format SVG ou PNG avec une taille minimum de 40x40px',

View File

@ -53,7 +53,7 @@ const translation = {
show: 'दिखाएं', show: 'दिखाएं',
hide: 'छुपाएं', hide: 'छुपाएं',
subTitle: 'कार्यप्रवाह विवरण', subTitle: 'कार्यप्रवाह विवरण',
showDesc: 'WebApp में वर्कफ़्लो विवरण दिखाएँ या छुपाएँ', showDesc: 'web app में वर्कफ़्लो विवरण दिखाएँ या छुपाएँ',
}, },
chatColorTheme: 'चैटबॉट का रंग थीम', chatColorTheme: 'चैटबॉट का रंग थीम',
chatColorThemeDesc: 'चैटबॉट का रंग थीम निर्धारित करें', chatColorThemeDesc: 'चैटबॉट का रंग थीम निर्धारित करें',
@ -77,8 +77,8 @@ const translation = {
sso: { sso: {
title: 'वेबएप एसएसओ', title: 'वेबएप एसएसओ',
label: 'SSO प्रमाणीकरण', label: 'SSO प्रमाणीकरण',
description: 'WebApp का उपयोग करने से पहले सभी उपयोगकर्ताओं को SSO के साथ लॉगिन करना आवश्यक है', description: 'web app का उपयोग करने से पहले सभी उपयोगकर्ताओं को SSO के साथ लॉगिन करना आवश्यक है',
tooltip: 'WebApp SSO को सक्षम करने के लिए व्यवस्थापक से संपर्क करें', tooltip: 'web app SSO को सक्षम करने के लिए व्यवस्थापक से संपर्क करें',
}, },
modalTip: 'क्लाइंट-साइड वेब अनुप्रयोग सेटिंग्स.', modalTip: 'क्लाइंट-साइड वेब अनुप्रयोग सेटिंग्स.',
}, },

View File

@ -161,9 +161,9 @@ const translation = {
}, },
}, },
answerIcon: { answerIcon: {
title: 'बदलने 🤖 के लिए WebApp चिह्न का उपयोग करें', title: 'बदलने 🤖 के लिए web app चिह्न का उपयोग करें',
descriptionInExplore: 'एक्सप्लोर में बदलने 🤖 के लिए वेबऐप आइकन का उपयोग करना है या नहीं', descriptionInExplore: 'एक्सप्लोर में बदलने 🤖 के लिए वेबऐप आइकन का उपयोग करना है या नहीं',
description: 'साझा अनुप्रयोग में प्रतिस्थापित 🤖 करने के लिए WebApp चिह्न का उपयोग करना है या नहीं', description: 'साझा अनुप्रयोग में प्रतिस्थापित 🤖 करने के लिए web app चिह्न का उपयोग करना है या नहीं',
}, },
importFromDSLFile: 'डीएसएल फ़ाइल से', importFromDSLFile: 'डीएसएल फ़ाइल से',
importFromDSLUrl: 'यूआरएल से', importFromDSLUrl: 'यूआरएल से',

View File

@ -5,7 +5,7 @@ const translation = {
suffix: 'स्वयं अपना ब्रांड चुनना।', suffix: 'स्वयं अपना ब्रांड चुनना।',
}, },
webapp: { webapp: {
title: 'WebApp का ब्रांड व्यक्तिकरण करें', title: 'web app का ब्रांड व्यक्तिकरण करें',
removeBrand: 'पावर्ड द्वारा डिफी हटाएं', removeBrand: 'पावर्ड द्वारा डिफी हटाएं',
changeLogo: 'पावर्ड द्वारा ब्रांड छवि बदले', changeLogo: 'पावर्ड द्वारा ब्रांड छवि बदले',
changeLogoTip: 'SVG या PNG प्रारूप के साथ न्यूनतम आकार 40x40px होना चाहिए', changeLogoTip: 'SVG या PNG प्रारूप के साथ न्यूनतम आकार 40x40px होना चाहिए',

View File

@ -33,27 +33,27 @@ const translation = {
overview: { overview: {
title: 'Panoramica', title: 'Panoramica',
appInfo: { appInfo: {
explanation: 'AI WebApp pronta all\'uso', explanation: 'AI web app pronta all\'uso',
accessibleAddress: 'URL Pubblico', accessibleAddress: 'URL Pubblico',
preview: 'Anteprima', preview: 'Anteprima',
regenerate: 'Rigenera', regenerate: 'Rigenera',
regenerateNotice: 'Vuoi rigenerare l\'URL pubblico?', regenerateNotice: 'Vuoi rigenerare l\'URL pubblico?',
preUseReminder: 'Attiva WebApp prima di continuare.', preUseReminder: 'Attiva web app prima di continuare.',
settings: { settings: {
entry: 'Impostazioni', entry: 'Impostazioni',
title: 'Impostazioni WebApp', title: 'Impostazioni web app',
webName: 'Nome WebApp', webName: 'Nome web app',
webDesc: 'Descrizione WebApp', webDesc: 'Descrizione web app',
webDescTip: webDescTip:
'Questo testo verrà visualizzato sul lato client, fornendo una guida di base su come utilizzare l\'applicazione', 'Questo testo verrà visualizzato sul lato client, fornendo una guida di base su come utilizzare l\'applicazione',
webDescPlaceholder: 'Inserisci la descrizione della WebApp', webDescPlaceholder: 'Inserisci la descrizione della web app',
language: 'Lingua', language: 'Lingua',
workflow: { workflow: {
title: 'Fasi del Workflow', title: 'Fasi del Workflow',
show: 'Mostra', show: 'Mostra',
hide: 'Nascondi', hide: 'Nascondi',
subTitle: 'Dettagli del flusso di lavoro', subTitle: 'Dettagli del flusso di lavoro',
showDesc: 'Mostrare o nascondere i dettagli del flusso di lavoro in WebApp', showDesc: 'Mostrare o nascondere i dettagli del flusso di lavoro in web app',
}, },
chatColorTheme: 'Tema colore chat', chatColorTheme: 'Tema colore chat',
chatColorThemeDesc: 'Imposta il tema colore del chatbot', chatColorThemeDesc: 'Imposta il tema colore del chatbot',
@ -78,9 +78,9 @@ const translation = {
}, },
sso: { sso: {
label: 'Autenticazione SSO', label: 'Autenticazione SSO',
title: 'WebApp SSO', title: 'web app SSO',
description: 'Tutti gli utenti devono effettuare l\'accesso con SSO prima di utilizzare WebApp', description: 'Tutti gli utenti devono effettuare l\'accesso con SSO prima di utilizzare web app',
tooltip: 'Contattare l\'amministratore per abilitare l\'SSO di WebApp', tooltip: 'Contattare l\'amministratore per abilitare l\'SSO di web app',
}, },
modalTip: 'Impostazioni dell\'app Web lato client.', modalTip: 'Impostazioni dell\'app Web lato client.',
}, },
@ -104,7 +104,7 @@ const translation = {
customize: { customize: {
way: 'modo', way: 'modo',
entry: 'Personalizza', entry: 'Personalizza',
title: 'Personalizza AI WebApp', title: 'Personalizza AI web app',
explanation: explanation:
'Puoi personalizzare il frontend della Web App per adattarla alle tue esigenze di scenario e stile.', 'Puoi personalizzare il frontend della Web App per adattarla alle tue esigenze di scenario e stile.',
way1: { way1: {

View File

@ -173,9 +173,9 @@ const translation = {
}, },
}, },
answerIcon: { answerIcon: {
description: 'Se utilizzare l\'icona WebApp per la sostituzione 🤖 nell\'applicazione condivisa', description: 'Se utilizzare l\'icona web app per la sostituzione 🤖 nell\'applicazione condivisa',
title: 'Usa l\'icona WebApp per sostituire 🤖', title: 'Usa l\'icona web app per sostituire 🤖',
descriptionInExplore: 'Se utilizzare l\'icona WebApp per sostituirla 🤖 in Esplora', descriptionInExplore: 'Se utilizzare l\'icona web app per sostituirla 🤖 in Esplora',
}, },
importFromDSLUrl: 'Dall\'URL', importFromDSLUrl: 'Dall\'URL',
importFromDSLFile: 'Da file DSL', importFromDSLFile: 'Da file DSL',

View File

@ -5,7 +5,7 @@ const translation = {
suffix: 'personalizzare il tuo marchio.', suffix: 'personalizzare il tuo marchio.',
}, },
webapp: { webapp: {
title: 'Personalizza il marchio WebApp', title: 'Personalizza il marchio web app',
removeBrand: 'Rimuovi Powered by Dify', removeBrand: 'Rimuovi Powered by Dify',
changeLogo: 'Cambia immagine del marchio Powered by', changeLogo: 'Cambia immagine del marchio Powered by',
changeLogoTip: 'Formato SVG o PNG con una dimensione minima di 40x40px', changeLogoTip: 'Formato SVG o PNG con una dimensione minima di 40x40px',

View File

@ -189,6 +189,36 @@ const translation = {
searchAllTemplate: 'すべてのテンプレートを検索...', searchAllTemplate: 'すべてのテンプレートを検索...',
}, },
showMyCreatedAppsOnly: '自分が作成したアプリ', showMyCreatedAppsOnly: '自分が作成したアプリ',
accessControl: 'Webアプリアクセス制御',
accessControlDialog: {
title: 'アクセス権限',
description: 'Webアプリのアクセス権限を設定します',
accessLabel: '誰がアクセスできますか',
accessItems: {
anyone: 'すべてのユーザー',
specific: '特定のグループメンバー',
organization: 'グループ内の全員',
},
groups_one: '{{count}} グループ',
groups_other: '{{count}} グループ',
members_one: '{{count}} メンバー',
members_other: '{{count}} メンバー',
noGroupsOrMembers: 'グループまたはメンバーが選択されていません',
webAppSSONotEnabledTip: 'Webアプリの認証方式設定については、企業管理者へご連絡ください。',
operateGroupAndMember: {
searchPlaceholder: 'グループやメンバーを検索',
allMembers: 'すべてのメンバー',
expand: '展開',
noResult: '結果がありません',
},
updateSuccess: '更新が成功しました',
},
publishApp: {
title: 'Webアプリへのアクセス権',
notSet: '未設定',
notSetDesc: '現在このWebアプリには誰もアクセスできません。権限を設定してください。',
},
noAccessPermission: 'Webアプリにアクセス権限がありません',
} }
export default translation export default translation

View File

@ -622,6 +622,7 @@ const translation = {
pagination: { pagination: {
perPage: 'ページあたりのアイテム数', perPage: 'ページあたりのアイテム数',
}, },
you: 'あなた',
} }
export default translation export default translation

View File

@ -105,6 +105,11 @@ const translation = {
licenseInactiveTip: 'ワークスペースの Dify Enterprise ライセンスが非アクティブです。Difyを引き続き使用するには、管理者に連絡してください。', licenseInactiveTip: 'ワークスペースの Dify Enterprise ライセンスが非アクティブです。Difyを引き続き使用するには、管理者に連絡してください。',
licenseExpired: 'ライセンスの有効期限が切れています', licenseExpired: 'ライセンスの有効期限が切れています',
licenseLostTip: 'Difyライセンスサーバーへの接続に失敗しました。続けてDifyを使用するために管理者に連絡してください。', licenseLostTip: 'Difyライセンスサーバーへの接続に失敗しました。続けてDifyを使用するために管理者に連絡してください。',
webapp: {
noLoginMethod: 'Webアプリに対して認証方法が構成されていません',
noLoginMethodTip: 'システム管理者に連絡して、認証方法を追加してください。',
disabled: 'Webアプリの認証が無効になっています。システム管理者に連絡して有効にしてください。直接アプリを使用してみてください。',
},
} }
export default translation export default translation

View File

@ -71,7 +71,7 @@ const translation = {
sso: { sso: {
label: 'SSO 인증', label: 'SSO 인증',
title: '웹앱 SSO', title: '웹앱 SSO',
tooltip: '관리자에게 문의하여 WebApp SSO를 사용하도록 설정합니다.', tooltip: '관리자에게 문의하여 web app SSO를 사용하도록 설정합니다.',
description: '모든 사용자는 WebApp을 사용하기 전에 SSO로 로그인해야 합니다.', description: '모든 사용자는 WebApp을 사용하기 전에 SSO로 로그인해야 합니다.',
}, },
modalTip: '클라이언트 쪽 웹앱 설정.', modalTip: '클라이언트 쪽 웹앱 설정.',

View File

@ -157,9 +157,9 @@ const translation = {
}, },
}, },
answerIcon: { answerIcon: {
description: 'WebApp 아이콘을 사용하여 공유 응용 프로그램에서 바꿀🤖지 여부', description: 'web app 아이콘을 사용하여 공유 응용 프로그램에서 바꿀🤖지 여부',
title: 'WebApp 아이콘을 사용하여 🤖', title: 'web app 아이콘을 사용하여 🤖',
descriptionInExplore: 'Explore에서 WebApp 아이콘을 사용하여 바꿀🤖지 여부', descriptionInExplore: 'Explore에서 web app 아이콘을 사용하여 바꿀🤖지 여부',
}, },
importFromDSL: 'DSL에서 가져오기', importFromDSL: 'DSL에서 가져오기',
importFromDSLFile: 'DSL 파일에서', importFromDSLFile: 'DSL 파일에서',

View File

@ -5,7 +5,7 @@ const translation = {
suffix: '브랜드를 사용자 정의하세요.', suffix: '브랜드를 사용자 정의하세요.',
}, },
webapp: { webapp: {
title: 'WebApp 브랜드 사용자 정의', title: 'web app 브랜드 사용자 정의',
removeBrand: 'Powered by Dify 삭제', removeBrand: 'Powered by Dify 삭제',
changeLogo: 'Powered by 브랜드 이미지 변경', changeLogo: 'Powered by 브랜드 이미지 변경',
changeLogoTip: '최소 크기 40x40px의 SVG 또는 PNG 형식', changeLogoTip: '최소 크기 40x40px의 SVG 또는 PNG 형식',

View File

@ -38,15 +38,15 @@ const translation = {
preview: 'Podgląd', preview: 'Podgląd',
regenerate: 'Wygeneruj ponownie', regenerate: 'Wygeneruj ponownie',
regenerateNotice: 'Czy chcesz wygenerować ponownie publiczny adres URL?', regenerateNotice: 'Czy chcesz wygenerować ponownie publiczny adres URL?',
preUseReminder: 'Przed kontynuowaniem włącz aplikację WebApp.', preUseReminder: 'Przed kontynuowaniem włącz aplikację web app.',
settings: { settings: {
entry: 'Ustawienia', entry: 'Ustawienia',
title: 'Ustawienia WebApp', title: 'Ustawienia web app',
webName: 'Nazwa WebApp', webName: 'Nazwa web app',
webDesc: 'Opis WebApp', webDesc: 'Opis web app',
webDescTip: webDescTip:
'Ten tekst będzie wyświetlany po stronie klienta, zapewniając podstawowe wskazówki, jak korzystać z aplikacji', 'Ten tekst będzie wyświetlany po stronie klienta, zapewniając podstawowe wskazówki, jak korzystać z aplikacji',
webDescPlaceholder: 'Wpisz opis WebApp', webDescPlaceholder: 'Wpisz opis web app',
language: 'Język', language: 'Język',
workflow: { workflow: {
title: 'Kroki przepływu pracy', title: 'Kroki przepływu pracy',

View File

@ -169,7 +169,7 @@ const translation = {
}, },
answerIcon: { answerIcon: {
description: 'Czy w aplikacji udostępnionej ma być używana ikona aplikacji internetowej do zamiany 🤖.', description: 'Czy w aplikacji udostępnionej ma być używana ikona aplikacji internetowej do zamiany 🤖.',
title: 'Użyj ikony WebApp, aby zastąpić 🤖', title: 'Użyj ikony web app, aby zastąpić 🤖',
descriptionInExplore: 'Czy używać ikony aplikacji internetowej do zastępowania 🤖 w Eksploruj', descriptionInExplore: 'Czy używać ikony aplikacji internetowej do zastępowania 🤖 w Eksploruj',
}, },
importFromDSL: 'Importowanie z DSL', importFromDSL: 'Importowanie z DSL',

View File

@ -30,26 +30,26 @@ const translation = {
overview: { overview: {
title: 'Visão Geral', title: 'Visão Geral',
appInfo: { appInfo: {
explanation: 'WebApp de IA Pronta para Uso', explanation: 'web app de IA Pronta para Uso',
accessibleAddress: 'URL Pública', accessibleAddress: 'URL Pública',
preview: 'Visualização', preview: 'Visualização',
regenerate: 'Regenerar', regenerate: 'Regenerar',
regenerateNotice: 'Você deseja regenerar a URL pública?', regenerateNotice: 'Você deseja regenerar a URL pública?',
preUseReminder: 'Por favor, ative o WebApp antes de continuar.', preUseReminder: 'Por favor, ative o web app antes de continuar.',
settings: { settings: {
entry: 'Configurações', entry: 'Configurações',
title: 'Configurações do WebApp', title: 'Configurações do web app',
webName: 'Nome do WebApp', webName: 'Nome do web app',
webDesc: 'Descrição do WebApp', webDesc: 'Descrição do web app',
webDescTip: 'Este texto será exibido no lado do cliente, fornecendo orientações básicas sobre como usar o aplicativo', webDescTip: 'Este texto será exibido no lado do cliente, fornecendo orientações básicas sobre como usar o aplicativo',
webDescPlaceholder: 'Insira a descrição do WebApp', webDescPlaceholder: 'Insira a descrição do web app',
language: 'Idioma', language: 'Idioma',
workflow: { workflow: {
title: 'Etapas do fluxo de trabalho', title: 'Etapas do fluxo de trabalho',
show: 'Mostrar', show: 'Mostrar',
hide: 'Ocultar', hide: 'Ocultar',
subTitle: 'Detalhes do fluxo de trabalho', subTitle: 'Detalhes do fluxo de trabalho',
showDesc: 'Mostrar ou ocultar detalhes do fluxo de trabalho no WebApp', showDesc: 'Mostrar ou ocultar detalhes do fluxo de trabalho no web app',
}, },
chatColorTheme: 'Tema de cor do chatbot', chatColorTheme: 'Tema de cor do chatbot',
chatColorThemeDesc: 'Defina o tema de cor do chatbot', chatColorThemeDesc: 'Defina o tema de cor do chatbot',
@ -69,10 +69,10 @@ const translation = {
copyrightTooltip: 'Por favor, atualize para o plano Professional ou superior', copyrightTooltip: 'Por favor, atualize para o plano Professional ou superior',
}, },
sso: { sso: {
tooltip: 'Entre em contato com o administrador para habilitar o SSO do WebApp', tooltip: 'Entre em contato com o administrador para habilitar o SSO do web app',
label: 'Autenticação SSO', label: 'Autenticação SSO',
title: 'WebApp SSO', title: 'web app SSO',
description: 'Todos os usuários devem fazer login com SSO antes de usar o WebApp', description: 'Todos os usuários devem fazer login com SSO antes de usar o web app',
}, },
modalTip: 'Configurações do aplicativo Web do lado do cliente.', modalTip: 'Configurações do aplicativo Web do lado do cliente.',
}, },
@ -94,7 +94,7 @@ const translation = {
customize: { customize: {
way: 'modo', way: 'modo',
entry: 'Personalizar', entry: 'Personalizar',
title: 'Personalizar WebApp de IA', title: 'Personalizar web app de IA',
explanation: 'Você pode personalizar a interface do usuário do Web App para atender às suas necessidades de cenário e estilo.', explanation: 'Você pode personalizar a interface do usuário do Web App para atender às suas necessidades de cenário e estilo.',
way1: { way1: {
name: 'Faça um fork do código do cliente, modifique-o e implante-o no Vercel (recomendado)', name: 'Faça um fork do código do cliente, modifique-o e implante-o no Vercel (recomendado)',

View File

@ -161,9 +161,9 @@ const translation = {
}, },
}, },
answerIcon: { answerIcon: {
descriptionInExplore: 'Se o ícone do WebApp deve ser usado para substituir 🤖 no Explore', descriptionInExplore: 'Se o ícone do web app deve ser usado para substituir 🤖 no Explore',
description: 'Se o ícone WebApp deve ser usado para substituir 🤖 no aplicativo compartilhado', description: 'Se o ícone web app deve ser usado para substituir 🤖 no aplicativo compartilhado',
title: 'Use o ícone do WebApp para substituir 🤖', title: 'Use o ícone do web app para substituir 🤖',
}, },
importFromDSLUrlPlaceholder: 'Cole o link DSL aqui', importFromDSLUrlPlaceholder: 'Cole o link DSL aqui',
importFromDSLUrl: 'Do URL', importFromDSLUrl: 'Do URL',

View File

@ -5,7 +5,7 @@ const translation = {
suffix: 'personalizar sua marca.', suffix: 'personalizar sua marca.',
}, },
webapp: { webapp: {
title: 'Personalizar marca do WebApp', title: 'Personalizar marca do web app',
removeBrand: 'Remover Powered by Dify', removeBrand: 'Remover Powered by Dify',
changeLogo: 'Alterar Imagem da Marca Powered by', changeLogo: 'Alterar Imagem da Marca Powered by',
changeLogoTip: 'Formato SVG ou PNG com tamanho mínimo de 40x40px', changeLogoTip: 'Formato SVG ou PNG com tamanho mínimo de 40x40px',

View File

@ -49,7 +49,7 @@ const translation = {
show: 'Afișați', show: 'Afișați',
hide: 'Ascundeți', hide: 'Ascundeți',
subTitle: 'Detalii despre fluxul de lucru', subTitle: 'Detalii despre fluxul de lucru',
showDesc: 'Afișarea sau ascunderea detaliilor fluxului de lucru în WebApp', showDesc: 'Afișarea sau ascunderea detaliilor fluxului de lucru în web app',
}, },
chatColorTheme: 'Tema de culoare a chatului', chatColorTheme: 'Tema de culoare a chatului',
chatColorThemeDesc: 'Setați tema de culoare a chatbotului', chatColorThemeDesc: 'Setați tema de culoare a chatbotului',
@ -70,9 +70,9 @@ const translation = {
}, },
sso: { sso: {
label: 'Autentificare SSO', label: 'Autentificare SSO',
title: 'WebApp SSO', title: 'web app SSO',
description: 'Toți utilizatorii trebuie să se conecteze cu SSO înainte de a utiliza WebApp', description: 'Toți utilizatorii trebuie să se conecteze cu SSO înainte de a utiliza web app',
tooltip: 'Contactați administratorul pentru a activa WebApp SSO', tooltip: 'Contactați administratorul pentru a activa web app SSO',
}, },
modalTip: 'Setările aplicației web pe partea clientului.', modalTip: 'Setările aplicației web pe partea clientului.',
}, },

View File

@ -161,9 +161,9 @@ const translation = {
}, },
}, },
answerIcon: { answerIcon: {
descriptionInExplore: 'Dacă să utilizați pictograma WebApp pentru a înlocui 🤖 în Explore', descriptionInExplore: 'Dacă să utilizați pictograma web app pentru a înlocui 🤖 în Explore',
description: 'Dacă se utilizează pictograma WebApp pentru a înlocui 🤖 în aplicația partajată', description: 'Dacă se utilizează pictograma web app pentru a înlocui 🤖 în aplicația partajată',
title: 'Utilizați pictograma WebApp pentru a înlocui 🤖', title: 'Utilizați pictograma web app pentru a înlocui 🤖',
}, },
importFromDSL: 'Import din DSL', importFromDSL: 'Import din DSL',
importFromDSLUrl: 'De la URL', importFromDSLUrl: 'De la URL',

View File

@ -5,7 +5,7 @@ const translation = {
suffix: 'să vă personalizați marca.', suffix: 'să vă personalizați marca.',
}, },
webapp: { webapp: {
title: 'Personalizați marca WebApp', title: 'Personalizați marca web app',
removeBrand: 'Eliminați "Powered by Dify"', removeBrand: 'Eliminați "Powered by Dify"',
changeLogo: 'Schimbați imaginea mărcii "Powered by"', changeLogo: 'Schimbați imaginea mărcii "Powered by"',
changeLogoTip: 'Format SVG sau PNG cu o dimensiune minimă de 40x40px', changeLogoTip: 'Format SVG sau PNG cu o dimensiune minimă de 40x40px',

View File

@ -57,9 +57,9 @@ const translation = {
invalidHexMessage: 'Неверное HEX-значение', invalidHexMessage: 'Неверное HEX-значение',
sso: { sso: {
label: 'SSO аутентификация', label: 'SSO аутентификация',
title: 'WebApp SSO', title: 'web app SSO',
description: 'Все пользователи должны войти в систему с помощью SSO перед использованием WebApp', description: 'Все пользователи должны войти в систему с помощью SSO перед использованием web app',
tooltip: 'Обратитесь к администратору, чтобы включить WebApp SSO', tooltip: 'Обратитесь к администратору, чтобы включить web app SSO',
}, },
more: { more: {
entry: 'Показать больше настроек', entry: 'Показать больше настроек',

View File

@ -165,9 +165,9 @@ const translation = {
}, },
}, },
answerIcon: { answerIcon: {
title: 'Использование значка WebApp для замены 🤖', title: 'Использование значка web app для замены 🤖',
description: 'Следует ли использовать значок WebApp для замены 🤖 в общем приложении', description: 'Следует ли использовать значок web app для замены 🤖 в общем приложении',
descriptionInExplore: 'Следует ли использовать значок WebApp для замены 🤖 в разделе "Обзор"', descriptionInExplore: 'Следует ли использовать значок web app для замены 🤖 в разделе "Обзор"',
}, },
mermaid: { mermaid: {
handDrawn: 'Рисованный', handDrawn: 'Рисованный',

View File

@ -109,9 +109,9 @@ const translation = {
image: 'Slika', image: 'Slika',
}, },
answerIcon: { answerIcon: {
title: 'Uporabite ikono WebApp za zamenjavo 🤖', title: 'Uporabite ikono web app za zamenjavo 🤖',
description: 'Ali uporabiti ikono WebApp za zamenjavo 🤖 v deljeni aplikaciji', description: 'Ali uporabiti ikono web app za zamenjavo 🤖 v deljeni aplikaciji',
descriptionInExplore: 'Ali uporabiti ikono WebApp za zamenjavo 🤖 v razdelku Razišči', descriptionInExplore: 'Ali uporabiti ikono web app za zamenjavo 🤖 v razdelku Razišči',
}, },
switch: 'Preklopi na Workflow Orchestrate', switch: 'Preklopi na Workflow Orchestrate',
switchTipStart: 'Za vas bo ustvarjena nova kopija aplikacije, ki bo preklopila na Workflow Orchestrate. Nova kopija ne bo ', switchTipStart: 'Za vas bo ustvarjena nova kopija aplikacije, ki bo preklopila na Workflow Orchestrate. Nova kopija ne bo ',

View File

@ -30,26 +30,26 @@ const translation = {
overview: { overview: {
title: 'ภาพรวม', title: 'ภาพรวม',
appInfo: { appInfo: {
explanation: 'AI WebApp พร้อมใช้งาน', explanation: 'AI web app พร้อมใช้งาน',
accessibleAddress: 'URL สาธารณะ', accessibleAddress: 'URL สาธารณะ',
preview: 'ดูตัวอย่าง', preview: 'ดูตัวอย่าง',
regenerate: 'สร้างใหม่', regenerate: 'สร้างใหม่',
regenerateNotice: 'คุณต้องการสร้าง URL สาธารณะใหม่หรือไม่', regenerateNotice: 'คุณต้องการสร้าง URL สาธารณะใหม่หรือไม่',
preUseReminder: 'โปรดเปิดใช้งาน WebApp ก่อนดําเนินการต่อ', preUseReminder: 'โปรดเปิดใช้งาน web app ก่อนดําเนินการต่อ',
settings: { settings: {
entry: 'การตั้งค่า', entry: 'การตั้งค่า',
title: 'การตั้งค่าเว็บแอป', title: 'การตั้งค่าเว็บแอป',
webName: 'ชื่อเว็บแอป', webName: 'ชื่อเว็บแอป',
webDesc: 'คําอธิบาย WebApp', webDesc: 'คําอธิบาย web app',
webDescTip: 'ข้อความนี้จะแสดงที่ฝั่งไคลเอ็นต์ โดยให้คําแนะนําพื้นฐานเกี่ยวกับวิธีการใช้แอปพลิเคชัน', webDescTip: 'ข้อความนี้จะแสดงที่ฝั่งไคลเอ็นต์ โดยให้คําแนะนําพื้นฐานเกี่ยวกับวิธีการใช้แอปพลิเคชัน',
webDescPlaceholder: 'ป้อนคําอธิบายของ WebApp', webDescPlaceholder: 'ป้อนคําอธิบายของ web app',
language: 'ภาษา', language: 'ภาษา',
workflow: { workflow: {
title: 'เวิร์กโฟลว์', title: 'เวิร์กโฟลว์',
subTitle: 'รายละเอียดเวิร์กโฟลว์', subTitle: 'รายละเอียดเวิร์กโฟลว์',
show: 'แสดง', show: 'แสดง',
hide: 'ซ่อน', hide: 'ซ่อน',
showDesc: 'แสดงหรือซ่อนรายละเอียดเวิร์กโฟลว์ใน WebApp', showDesc: 'แสดงหรือซ่อนรายละเอียดเวิร์กโฟลว์ใน web app',
}, },
chatColorTheme: 'ธีมสีแชท', chatColorTheme: 'ธีมสีแชท',
chatColorThemeDesc: 'กําหนดธีมสีของแชทบอท', chatColorThemeDesc: 'กําหนดธีมสีของแชทบอท',
@ -58,8 +58,8 @@ const translation = {
sso: { sso: {
label: 'การรับรองความถูกต้องของ SSO', label: 'การรับรองความถูกต้องของ SSO',
title: 'เว็บแอป SSO', title: 'เว็บแอป SSO',
description: 'ผู้ใช้ทุกคนต้องเข้าสู่ระบบด้วย SSO ก่อนใช้ WebApp', description: 'ผู้ใช้ทุกคนต้องเข้าสู่ระบบด้วย SSO ก่อนใช้ web app',
tooltip: 'ติดต่อผู้ดูแลระบบเพื่อเปิดใช้ WebApp SSO', tooltip: 'ติดต่อผู้ดูแลระบบเพื่อเปิดใช้ web app SSO',
}, },
more: { more: {
entry: 'แสดงการตั้งค่าเพิ่มเติม', entry: 'แสดงการตั้งค่าเพิ่มเติม',
@ -94,7 +94,7 @@ const translation = {
customize: { customize: {
way: 'วิธี', way: 'วิธี',
entry: 'ปรับแต่ง', entry: 'ปรับแต่ง',
title: 'ปรับแต่ง AI WebApp', title: 'ปรับแต่ง AI web app',
explanation: 'คุณสามารถปรับแต่งส่วนหน้าของ Web App ให้เหมาะกับสถานการณ์และความต้องการสไตล์ของคุณได้', explanation: 'คุณสามารถปรับแต่งส่วนหน้าของ Web App ให้เหมาะกับสถานการณ์และความต้องการสไตล์ของคุณได้',
way1: { way1: {
name: 'แยกรหัสไคลเอ็นต์ แก้ไข และปรับใช้กับ Vercel (แนะนํา)', name: 'แยกรหัสไคลเอ็นต์ แก้ไข และปรับใช้กับ Vercel (แนะนํา)',

View File

@ -105,9 +105,9 @@ const translation = {
image: 'ภาพ', image: 'ภาพ',
}, },
answerIcon: { answerIcon: {
title: 'ใช้ไอคอน WebApp เพื่อแทนที่ 🤖', title: 'ใช้ไอคอน web app เพื่อแทนที่ 🤖',
description: 'จะใช้ไอคอน WebApp เพื่อแทนที่🤖ในโปรเจกต์ที่ใช้ร่วมกันหรือไม่', description: 'จะใช้ไอคอน web app เพื่อแทนที่🤖ในโปรเจกต์ที่ใช้ร่วมกันหรือไม่',
descriptionInExplore: 'จะใช้ไอคอน WebApp เพื่อแทนที่🤖ใน Explore หรือไม่', descriptionInExplore: 'จะใช้ไอคอน web app เพื่อแทนที่🤖ใน Explore หรือไม่',
}, },
switch: 'เปลี่ยนไปใช้ Workflow Orchestrate', switch: 'เปลี่ยนไปใช้ Workflow Orchestrate',
switchTipStart: 'สําเนาโปรเจกต์ใหม่จะถูกสร้างขึ้นสําหรับคุณ และสําเนาใหม่จะเปลี่ยนเป็น Workflow Orchestration', switchTipStart: 'สําเนาโปรเจกต์ใหม่จะถูกสร้างขึ้นสําหรับคุณ และสําเนาใหม่จะเปลี่ยนเป็น Workflow Orchestration',

View File

@ -5,7 +5,7 @@ const translation = {
suffix: 'ปรับแต่งแบรนด์ของคุณ', suffix: 'ปรับแต่งแบรนด์ของคุณ',
}, },
webapp: { webapp: {
title: 'ปรับแต่งแบรนด์ WebApp', title: 'ปรับแต่งแบรนด์ web app',
removeBrand: 'ลบ ขับเคลื่อนโดย Dify', removeBrand: 'ลบ ขับเคลื่อนโดย Dify',
changeLogo: 'การเปลี่ยนแปลงที่ขับเคลื่อนโดยภาพลักษณ์ของแบรนด์', changeLogo: 'การเปลี่ยนแปลงที่ขับเคลื่อนโดยภาพลักษณ์ของแบรนด์',
changeLogoTip: 'รูปแบบ SVG หรือ PNG ที่มีขนาดขั้นต่ํา 40x40px', changeLogoTip: 'รูปแบบ SVG หรือ PNG ที่มีขนาดขั้นต่ํา 40x40px',

View File

@ -30,25 +30,25 @@ const translation = {
overview: { overview: {
title: 'Genel Bakış', title: 'Genel Bakış',
appInfo: { appInfo: {
explanation: 'Kullanıma hazır AI WebApp', explanation: 'Kullanıma hazır AI web app',
accessibleAddress: 'Genel URL', accessibleAddress: 'Genel URL',
preview: 'Önizleme', preview: 'Önizleme',
regenerate: 'Yeniden Oluştur', regenerate: 'Yeniden Oluştur',
regenerateNotice: 'Genel URL\'yi yeniden oluşturmak istiyor musunuz?', regenerateNotice: 'Genel URL\'yi yeniden oluşturmak istiyor musunuz?',
preUseReminder: 'Devam etmeden önce WebApp\'i etkinleştirin.', preUseReminder: 'Devam etmeden önce web app\'i etkinleştirin.',
settings: { settings: {
entry: 'Ayarlar', entry: 'Ayarlar',
title: 'WebApp Ayarları', title: 'web app Ayarları',
webName: 'WebApp İsmi', webName: 'web app İsmi',
webDesc: 'WebApp Açıklaması', webDesc: 'web app Açıklaması',
webDescTip: 'Bu metin, uygulamanın nasıl kullanılacağına dair temel açıklamalar sağlar ve istemci tarafında görüntülenir', webDescTip: 'Bu metin, uygulamanın nasıl kullanılacağına dair temel açıklamalar sağlar ve istemci tarafında görüntülenir',
webDescPlaceholder: 'WebApp\'in açıklamasını girin', webDescPlaceholder: 'web app\'in açıklamasını girin',
language: 'Dil', language: 'Dil',
workflow: { workflow: {
title: 'Workflow Adımları', title: 'Workflow Adımları',
show: 'Göster', show: 'Göster',
hide: 'Gizle', hide: 'Gizle',
showDesc: 'WebApp\'te iş akışı ayrıntılarını gösterme veya gizleme', showDesc: 'web app\'te iş akışı ayrıntılarını gösterme veya gizleme',
subTitle: 'İş Akışı Detayları', subTitle: 'İş Akışı Detayları',
}, },
chatColorTheme: 'Sohbet renk teması', chatColorTheme: 'Sohbet renk teması',
@ -69,10 +69,10 @@ const translation = {
copyrightTooltip: 'Lütfen Profesyonel plana veya daha yüksek bir plana yükseltin', copyrightTooltip: 'Lütfen Profesyonel plana veya daha yüksek bir plana yükseltin',
}, },
sso: { sso: {
title: 'WebApp SSO\'su', title: 'web app SSO\'su',
tooltip: 'WebApp SSO\'yu etkinleştirmek için yöneticiyle iletişime geçin', tooltip: 'web app SSO\'yu etkinleştirmek için yöneticiyle iletişime geçin',
label: 'SSO Kimlik Doğrulaması', label: 'SSO Kimlik Doğrulaması',
description: 'Tüm kullanıcıların WebApp\'i kullanmadan önce SSO ile oturum açmaları gerekir', description: 'Tüm kullanıcıların web app\'i kullanmadan önce SSO ile oturum açmaları gerekir',
}, },
modalTip: 'İstemci tarafı web uygulaması ayarları.', modalTip: 'İstemci tarafı web uygulaması ayarları.',
}, },
@ -94,7 +94,7 @@ const translation = {
customize: { customize: {
way: 'yol', way: 'yol',
entry: 'Özelleştir', entry: 'Özelleştir',
title: 'AI WebApp\'i Özelleştirin', title: 'AI web app\'i Özelleştirin',
explanation: 'Web Uygulamasının ön yüzünü senaryo ve stil ihtiyaçlarınıza uygun şekilde özelleştirebilirsiniz.', explanation: 'Web Uygulamasının ön yüzünü senaryo ve stil ihtiyaçlarınıza uygun şekilde özelleştirebilirsiniz.',
way1: { way1: {
name: 'İstemci kodunu forklayarak değiştirin ve Vercel\'e dağıtın (önerilen)', name: 'İstemci kodunu forklayarak değiştirin ve Vercel\'e dağıtın (önerilen)',

View File

@ -161,9 +161,9 @@ const translation = {
}, },
}, },
answerIcon: { answerIcon: {
descriptionInExplore: 'Keşfet\'te değiştirilecek 🤖 WebApp simgesinin kullanılıp kullanılmayacağı', descriptionInExplore: 'Keşfet\'te değiştirilecek 🤖 web app simgesinin kullanılıp kullanılmayacağı',
title: 'Değiştirmek 🤖 için WebApp simgesini kullanın', title: 'Değiştirmek 🤖 için web app simgesini kullanın',
description: 'Paylaşılan uygulamada değiştirmek 🤖 için WebApp simgesinin kullanılıp kullanılmayacağı', description: 'Paylaşılan uygulamada değiştirmek 🤖 için web app simgesinin kullanılıp kullanılmayacağı',
}, },
mermaid: { mermaid: {
handDrawn: 'Elle çizilmiş', handDrawn: 'Elle çizilmiş',

View File

@ -5,7 +5,7 @@ const translation = {
suffix: '.', suffix: '.',
}, },
webapp: { webapp: {
title: 'WebApp markasını özelleştir', title: 'web app markasını özelleştir',
removeBrand: 'Powered by Dify\'i kaldır', removeBrand: 'Powered by Dify\'i kaldır',
changeLogo: 'Powered by Brand Resmini Değiştir', changeLogo: 'Powered by Brand Resmini Değiştir',
changeLogoTip: 'SVG veya PNG formatında, en az 40x40px boyutunda', changeLogoTip: 'SVG veya PNG formatında, en az 40x40px boyutunda',

View File

@ -69,9 +69,9 @@ const translation = {
copyrightTooltip: 'Будь ласка, перейдіть на тарифний план «Professional» або вище', copyrightTooltip: 'Будь ласка, перейдіть на тарифний план «Professional» або вище',
}, },
sso: { sso: {
title: 'Єдиний вхід для WebApp', title: 'Єдиний вхід для web app',
description: 'Усі користувачі повинні увійти в систему за допомогою єдиного входу перед використанням WebApp', description: 'Усі користувачі повинні увійти в систему за допомогою єдиного входу перед використанням web app',
tooltip: 'Зверніться до адміністратора, щоб увімкнути єдиний вхід WebApp', tooltip: 'Зверніться до адміністратора, щоб увімкнути єдиний вхід web app',
label: 'Автентифікація за допомогою єдиного входу', label: 'Автентифікація за допомогою єдиного входу',
}, },
modalTip: 'Налаштування веб-додатку на стороні клієнта.', modalTip: 'Налаштування веб-додатку на стороні клієнта.',

View File

@ -161,8 +161,8 @@ const translation = {
}, },
}, },
answerIcon: { answerIcon: {
title: 'Використовуйте піктограму WebApp для заміни 🤖', title: 'Використовуйте піктограму web app для заміни 🤖',
description: 'Чи слід використовувати піктограму WebApp для заміни 🤖 у спільній програмі', description: 'Чи слід використовувати піктограму web app для заміни 🤖 у спільній програмі',
descriptionInExplore: 'Чи використовувати піктограму веб-програми для заміни 🤖 в Огляді', descriptionInExplore: 'Чи використовувати піктограму веб-програми для заміни 🤖 в Огляді',
}, },
importFromDSLUrl: 'З URL', importFromDSLUrl: 'З URL',

View File

@ -5,7 +5,7 @@ const translation = {
suffix: ', щоб налаштувати свій бренд.', suffix: ', щоб налаштувати свій бренд.',
}, },
webapp: { webapp: {
title: 'Налаштувати бренд для WebApp', title: 'Налаштувати бренд для web app',
removeBrand: 'Видалити Powered by Dify', removeBrand: 'Видалити Powered by Dify',
changeLogo: 'Змінити зображення бренду "Powered by"', changeLogo: 'Змінити зображення бренду "Powered by"',
changeLogoTip: 'Формат SVG або PNG з мінімальним розміром 40x40 пікселів', changeLogoTip: 'Формат SVG або PNG з мінімальним розміром 40x40 пікселів',

View File

@ -48,7 +48,7 @@ const translation = {
title: 'Các bước quy trình', title: 'Các bước quy trình',
show: 'Hiển thị', show: 'Hiển thị',
hide: 'Ẩn', hide: 'Ẩn',
showDesc: 'Hiển thị hoặc ẩn chi tiết dòng công việc trong WebApp', showDesc: 'Hiển thị hoặc ẩn chi tiết dòng công việc trong web app',
subTitle: 'Chi tiết quy trình làm việc', subTitle: 'Chi tiết quy trình làm việc',
}, },
chatColorTheme: 'Giao diện màu trò chuyện', chatColorTheme: 'Giao diện màu trò chuyện',
@ -70,8 +70,8 @@ const translation = {
}, },
sso: { sso: {
title: 'SSO ứng dụng web', title: 'SSO ứng dụng web',
description: 'Tất cả người dùng được yêu cầu đăng nhập bằng SSO trước khi sử dụng WebApp', description: 'Tất cả người dùng được yêu cầu đăng nhập bằng SSO trước khi sử dụng web app',
tooltip: 'Liên hệ với quản trị viên để bật SSO WebApp', tooltip: 'Liên hệ với quản trị viên để bật SSO web app',
label: 'Xác thực SSO', label: 'Xác thực SSO',
}, },
modalTip: 'Cài đặt ứng dụng web phía máy khách.', modalTip: 'Cài đặt ứng dụng web phía máy khách.',

View File

@ -161,9 +161,9 @@ const translation = {
}, },
}, },
answerIcon: { answerIcon: {
description: 'Có nên sử dụng biểu tượng WebApp để thay thế 🤖 trong ứng dụng được chia sẻ hay không', description: 'Có nên sử dụng biểu tượng web app để thay thế 🤖 trong ứng dụng được chia sẻ hay không',
descriptionInExplore: 'Có nên sử dụng biểu tượng WebApp để thay thế 🤖 trong Khám phá hay không', descriptionInExplore: 'Có nên sử dụng biểu tượng web app để thay thế 🤖 trong Khám phá hay không',
title: 'Sử dụng biểu tượng WebApp để thay thế 🤖', title: 'Sử dụng biểu tượng web app để thay thế 🤖',
}, },
importFromDSLFile: 'Từ tệp DSL', importFromDSLFile: 'Từ tệp DSL',
importFromDSL: 'Nhập từ DSL', importFromDSL: 'Nhập từ DSL',

View File

@ -5,7 +5,7 @@ const translation = {
suffix: 'tùy chỉnh thương hiệu.', suffix: 'tùy chỉnh thương hiệu.',
}, },
webapp: { webapp: {
title: 'Tùy chỉnh thương hiệu WebApp', title: 'Tùy chỉnh thương hiệu web app',
removeBrand: 'Xóa "Được hỗ trợ bởi Dify"', removeBrand: 'Xóa "Được hỗ trợ bởi Dify"',
changeLogo: 'Thay đổi logo "Được hỗ trợ bởi"', changeLogo: 'Thay đổi logo "Được hỗ trợ bởi"',
changeLogoTip: 'Định dạng SVG hoặc PNG với kích thước tối thiểu 40x40px', changeLogoTip: 'Định dạng SVG hoặc PNG với kích thước tối thiểu 40x40px',

View File

@ -29,7 +29,7 @@ const translation = {
noOutput: '无输出', noOutput: '无输出',
element: { element: {
title: '这里有人吗', title: '这里有人吗',
content: '在这里观测和标注最终用户和 AI 应用程序之间的交互,以不断提高 AI 的准确性。您可以<testLink>试试</testLink> WebApp 或<shareLink>分享</shareLink>出去,然后返回此页面。', content: '在这里观测和标注最终用户和 AI 应用程序之间的交互,以不断提高 AI 的准确性。您可以<testLink>试试</testLink> web app 或<shareLink>分享</shareLink>出去,然后返回此页面。',
}, },
}, },
}, },

View File

@ -30,7 +30,7 @@ const translation = {
overview: { overview: {
title: '概览', title: '概览',
appInfo: { appInfo: {
explanation: '开箱即用的 AI WebApp', explanation: '开箱即用的 AI web app',
accessibleAddress: '公开访问 URL', accessibleAddress: '公开访问 URL',
preview: '预览', preview: '预览',
regenerate: '重新生成', regenerate: '重新生成',
@ -38,19 +38,19 @@ const translation = {
preUseReminder: '使用前请先打开开关', preUseReminder: '使用前请先打开开关',
settings: { settings: {
entry: '设置', entry: '设置',
title: 'WebApp 设置', title: 'web app 设置',
modalTip: '客户端 WebApp 设置。', modalTip: '客户端 web app 设置。',
webName: 'WebApp 名称', webName: 'web app 名称',
webDesc: 'WebApp 描述', webDesc: 'web app 描述',
webDescTip: '以下文字将展示在客户端中,对应用进行说明和使用上的基本引导', webDescTip: '以下文字将展示在客户端中,对应用进行说明和使用上的基本引导',
webDescPlaceholder: '请输入 WebApp 的描述', webDescPlaceholder: '请输入 web app 的描述',
language: '语言', language: '语言',
workflow: { workflow: {
title: '工作流', title: '工作流',
subTitle: '工作流详情', subTitle: '工作流详情',
show: '显示', show: '显示',
hide: '隐藏', hide: '隐藏',
showDesc: '在 WebApp 中展示或者隐藏工作流详情', showDesc: '在 web app 中展示或者隐藏工作流详情',
}, },
chatColorTheme: '聊天颜色主题', chatColorTheme: '聊天颜色主题',
chatColorThemeDesc: '设置聊天机器人的颜色主题', chatColorThemeDesc: '设置聊天机器人的颜色主题',
@ -58,14 +58,14 @@ const translation = {
invalidHexMessage: '无效的十六进制值', invalidHexMessage: '无效的十六进制值',
sso: { sso: {
label: '单点登录认证', label: '单点登录认证',
title: 'WebApp SSO 认证', title: 'web app SSO 认证',
description: '启用后,所有用户都需要先进行 SSO 认证才能访问', description: '启用后,所有用户都需要先进行 SSO 认证才能访问',
tooltip: '联系管理员以开启 WebApp SSO 认证', tooltip: '联系管理员以开启 web app SSO 认证',
}, },
more: { more: {
entry: '展示更多设置', entry: '展示更多设置',
copyright: '版权', copyright: '版权',
copyrightTip: '在 WebApp 中展示版权信息', copyrightTip: '在 web app 中展示版权信息',
copyrightTooltip: '请升级到专业版或者更高', copyrightTooltip: '请升级到专业版或者更高',
copyRightPlaceholder: '请输入作者或组织名称', copyRightPlaceholder: '请输入作者或组织名称',
privacyPolicy: '隐私政策', privacyPolicy: '隐私政策',
@ -94,7 +94,7 @@ const translation = {
customize: { customize: {
way: '方法', way: '方法',
entry: '定制化', entry: '定制化',
title: '定制化 AI WebApp', title: '定制化 AI web app',
explanation: '你可以定制化 Web App 前端以符合你的情景与风格需求', explanation: '你可以定制化 Web App 前端以符合你的情景与风格需求',
way1: { way1: {
name: 'Fork 客户端代码修改后部署到 Vercel推荐', name: 'Fork 客户端代码修改后部署到 Vercel推荐',

View File

@ -113,9 +113,9 @@ const translation = {
image: '图片', image: '图片',
}, },
answerIcon: { answerIcon: {
title: '使用 WebApp 图标替换 🤖', title: '使用 web app 图标替换 🤖',
description: '是否使用 WebApp 图标替换分享的应用界面中的 🤖', description: '是否使用 web app 图标替换分享的应用界面中的 🤖',
descriptionInExplore: '是否使用 WebApp 图标替换 Explore 界面中的 🤖', descriptionInExplore: '是否使用 web app 图标替换 Explore 界面中的 🤖',
}, },
switch: '迁移为工作流编排', switch: '迁移为工作流编排',
switchTipStart: '将为您创建一个使用工作流编排的新应用。新应用将', switchTipStart: '将为您创建一个使用工作流编排的新应用。新应用将',
@ -175,6 +175,36 @@ const translation = {
}, },
openInExplore: '在“探索”中打开', openInExplore: '在“探索”中打开',
showMyCreatedAppsOnly: '我创建的', showMyCreatedAppsOnly: '我创建的',
accessControl: 'Web 应用访问控制',
accessControlDialog: {
title: 'Web 应用访问权限',
description: '设置 web 应用访问权限。',
accessLabel: '谁可以访问',
accessItems: {
anyone: '任何人',
specific: '特定组或成员',
organization: '组织内任何人',
},
groups_one: '{{count}} 个组',
groups_other: '{{count}} 个组',
members_one: '{{count}} 个成员',
members_other: '{{count}} 个成员',
noGroupsOrMembers: '未选择分组或成员',
webAppSSONotEnabledTip: '请联系企业管理员配置 web 应用的身份认证方式。',
operateGroupAndMember: {
searchPlaceholder: '搜索组或成员',
allMembers: '所有成员',
expand: '展开',
noResult: '没有结果',
},
updateSuccess: '更新成功',
},
publishApp: {
title: '谁可以访问 web 应用',
notSet: '未设置',
notSetDesc: '当前任何人都无法访问 Web 应用。请设置访问权限。',
},
noAccessPermission: '没有权限访问 web app ',
} }
export default translation export default translation

View File

@ -622,6 +622,7 @@ const translation = {
pagination: { pagination: {
perPage: '每页显示', perPage: '每页显示',
}, },
you: '你',
} }
export default translation export default translation

View File

@ -5,7 +5,7 @@ const translation = {
suffix: '定制您的品牌。', suffix: '定制您的品牌。',
}, },
webapp: { webapp: {
title: '定制 WebApp 品牌', title: '定制 web app 品牌',
removeBrand: '移除 Powered by Dify', removeBrand: '移除 Powered by Dify',
changeLogo: '更改 Powered by Brand 图片', changeLogo: '更改 Powered by Brand 图片',
changeLogoTip: 'SVG 或 PNG 格式,最小尺寸为 40x40px', changeLogoTip: 'SVG 或 PNG 格式,最小尺寸为 40x40px',

View File

@ -105,6 +105,11 @@ const translation = {
licenseLostTip: '无法连接 Dify 许可证服务器,请联系管理员以继续使用 Dify。', licenseLostTip: '无法连接 Dify 许可证服务器,请联系管理员以继续使用 Dify。',
licenseInactive: '许可证未激活', licenseInactive: '许可证未激活',
licenseInactiveTip: '您所在空间的 Dify Enterprise 许可证尚未激活,请联系管理员以继续使用 Dify。', licenseInactiveTip: '您所在空间的 Dify Enterprise 许可证尚未激活,请联系管理员以继续使用 Dify。',
webapp: {
noLoginMethod: 'Web 应用未配置身份认证方式',
noLoginMethodTip: '请联系系统管理员添加身份认证方式',
disabled: 'Web 应用身份认证已禁用,请联系系统管理员启用。您也可以尝试直接使用应用。',
},
} }
export default translation export default translation

View File

@ -29,7 +29,7 @@ const translation = {
noOutput: '無輸出', noOutput: '無輸出',
element: { element: {
title: '這裡有人嗎', title: '這裡有人嗎',
content: '在這裡觀測和標註終端使用者和 AI 應用程式之間的互動,以不斷提高 AI 的準確性。您可以<testLink>試試</testLink> WebApp 或<shareLink>分享</shareLink>出去,然後返回此頁面。', content: '在這裡觀測和標註終端使用者和 AI 應用程式之間的互動,以不斷提高 AI 的準確性。您可以<testLink>試試</testLink> web app 或<shareLink>分享</shareLink>出去,然後返回此頁面。',
}, },
}, },
}, },

View File

@ -30,7 +30,7 @@ const translation = {
overview: { overview: {
title: '概覽', title: '概覽',
appInfo: { appInfo: {
explanation: '開箱即用的 AI WebApp', explanation: '開箱即用的 AI web app',
accessibleAddress: '公開訪問 URL', accessibleAddress: '公開訪問 URL',
preview: '預覽', preview: '預覽',
regenerate: '重新生成', regenerate: '重新生成',
@ -38,18 +38,18 @@ const translation = {
preUseReminder: '使用前請先開啟開關', preUseReminder: '使用前請先開啟開關',
settings: { settings: {
entry: '設定', entry: '設定',
title: 'WebApp 設定', title: 'web app 設定',
webName: 'WebApp 名稱', webName: 'web app 名稱',
webDesc: 'WebApp 描述', webDesc: 'web app 描述',
webDescTip: '以下文字將展示在客戶端中,對應用進行說明和使用上的基本引導', webDescTip: '以下文字將展示在客戶端中,對應用進行說明和使用上的基本引導',
webDescPlaceholder: '請輸入 WebApp 的描述', webDescPlaceholder: '請輸入 web app 的描述',
language: '語言', language: '語言',
workflow: { workflow: {
title: '工作流程步驟', title: '工作流程步驟',
show: '展示', show: '展示',
hide: '隱藏', hide: '隱藏',
subTitle: '工作流詳細資訊', subTitle: '工作流詳細資訊',
showDesc: '在 WebApp 中顯示或隱藏工作流詳細資訊', showDesc: '在 web app 中顯示或隱藏工作流詳細資訊',
}, },
chatColorTheme: '聊天顏色主題', chatColorTheme: '聊天顏色主題',
chatColorThemeDesc: '設定聊天機器人的顏色主題', chatColorThemeDesc: '設定聊天機器人的顏色主題',
@ -69,9 +69,9 @@ const translation = {
copyrightTooltip: '請升級至專業計劃或以上', copyrightTooltip: '請升級至專業計劃或以上',
}, },
sso: { sso: {
description: '所有使用者在使用 WebApp 之前都需要使用 SSO 登錄', description: '所有使用者在使用 web app 之前都需要使用 SSO 登錄',
title: 'WebApp SSO', title: 'web app SSO',
tooltip: '聯繫管理員以啟用 WebApp SSO', tooltip: '聯繫管理員以啟用 web app SSO',
label: 'SSO 身份驗證', label: 'SSO 身份驗證',
}, },
modalTip: '用戶端 Web 應用程式設置。', modalTip: '用戶端 Web 應用程式設置。',
@ -94,7 +94,7 @@ const translation = {
customize: { customize: {
way: '方法', way: '方法',
entry: '定製化', entry: '定製化',
title: '定製化 AI WebApp', title: '定製化 AI web app',
explanation: '你可以定製化 Web App 前端以符合你的情景與風格需求', explanation: '你可以定製化 Web App 前端以符合你的情景與風格需求',
way1: { way1: {
name: 'Fork 客戶端程式碼修改後部署到 Vercel推薦', name: 'Fork 客戶端程式碼修改後部署到 Vercel推薦',

View File

@ -160,9 +160,9 @@ const translation = {
}, },
}, },
answerIcon: { answerIcon: {
descriptionInExplore: '是否使用 WebApp 圖示在 Explore 中取代 🤖', descriptionInExplore: '是否使用 web app 圖示在 Explore 中取代 🤖',
title: '使用 WebApp 圖示取代 🤖', title: '使用 web app 圖示取代 🤖',
description: '是否在共享應用程式中使用 WebApp 圖示進行取代 🤖', description: '是否在共享應用程式中使用 web app 圖示進行取代 🤖',
}, },
importFromDSLUrl: '寄件者 URL', importFromDSLUrl: '寄件者 URL',
importFromDSL: '從 DSL 導入', importFromDSL: '從 DSL 導入',

View File

@ -5,7 +5,7 @@ const translation = {
suffix: '定製您的品牌。', suffix: '定製您的品牌。',
}, },
webapp: { webapp: {
title: '定製 WebApp 品牌', title: '定製 web app 品牌',
removeBrand: '移除 Powered by Dify', removeBrand: '移除 Powered by Dify',
changeLogo: '更改 Powered by Brand 圖片', changeLogo: '更改 Powered by Brand 圖片',
changeLogoTip: 'SVG 或 PNG 格式,最小尺寸為 40x40px', changeLogoTip: 'SVG 或 PNG 格式,最小尺寸為 40x40px',

View File

@ -0,0 +1,29 @@
export enum SubjectType {
GROUP = 'group',
ACCOUNT = 'account',
}
export enum AccessMode {
PUBLIC = 'public',
SPECIFIC_GROUPS_MEMBERS = 'private',
ORGANIZATION = 'private_all',
}
export type AccessControlGroup = {
'id': 'string'
'name': 'string'
'groupSize': 5
}
export type AccessControlAccount = {
'id': 'string'
'name': 'string'
'email': 'string'
'avatar': 'string'
'avatarUrl': 'string'
}
export type SubjectGroup = { subjectId: string; subjectType: SubjectType; groupData: AccessControlGroup }
export type SubjectAccount = { subjectId: string; subjectType: SubjectType; accountData: AccessControlAccount }
export type Subject = SubjectGroup | SubjectAccount

View File

@ -1,5 +1,5 @@
import type { LangFuseConfig, LangSmithConfig, OpikConfig, TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type' import type { LangFuseConfig, LangSmithConfig, OpikConfig, TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type'
import type { App, AppSSO, AppTemplate, SiteConfig } from '@/types/app' import type { App, AppTemplate, SiteConfig } from '@/types/app'
/* export type App = { /* export type App = {
id: string id: string
@ -89,8 +89,6 @@ export type DSLImportResponse = {
error: string error: string
} }
export type AppSSOResponse = { enabled: AppSSO['enable_sso'] }
export type AppTemplatesResponse = { export type AppTemplatesResponse = {
data: AppTemplate[] data: AppTemplate[]
} }

View File

@ -0,0 +1,88 @@
import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { get, post } from './base'
import { getAppAccessMode, getUserCanAccess } from './share'
import type { AccessControlAccount, AccessControlGroup, AccessMode, Subject } from '@/models/access-control'
import type { App } from '@/types/app'
const NAME_SPACE = 'access-control'
export const useAppWhiteListSubjects = (appId: string | undefined, enabled: boolean) => {
return useQuery({
queryKey: [NAME_SPACE, 'app-whitelist-subjects', appId],
queryFn: () => get<{ groups: AccessControlGroup[]; members: AccessControlAccount[] }>(`/enterprise/webapp/app/subjects?appId=${appId}`),
enabled: !!appId && enabled,
staleTime: 0,
gcTime: 0,
})
}
type SearchResults = {
currPage: number
totalPages: number
subjects: Subject[]
hasMore: boolean
}
export const useSearchForWhiteListCandidates = (query: { keyword?: string; groupId?: AccessControlGroup['id']; resultsPerPage?: number }, enabled: boolean) => {
return useInfiniteQuery({
queryKey: [NAME_SPACE, 'app-whitelist-candidates', query],
queryFn: ({ pageParam }) => {
const params = new URLSearchParams()
Object.keys(query).forEach((key) => {
const typedKey = key as keyof typeof query
if (query[typedKey])
params.append(key, `${query[typedKey]}`)
})
params.append('pageNumber', `${pageParam}`)
return get<SearchResults>(`/enterprise/webapp/app/subject/search?${new URLSearchParams(params).toString()}`)
},
initialPageParam: 1,
getNextPageParam: (lastPage) => {
if (lastPage.hasMore)
return lastPage.currPage + 1
return undefined
},
enabled,
})
}
type UpdateAccessModeParams = {
appId: App['id']
subjects?: Pick<Subject, 'subjectId' | 'subjectType'>[]
accessMode: AccessMode
}
export const useUpdateAccessMode = () => {
const queryClient = useQueryClient()
return useMutation({
mutationKey: [NAME_SPACE, 'update-access-mode'],
mutationFn: (params: UpdateAccessModeParams) => {
return post('/enterprise/webapp/app/access-mode', { body: params })
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'app-whitelist-subjects'],
})
},
})
}
export const useGetAppAccessMode = ({ appId, isInstalledApp = true }: { appId?: string; isInstalledApp?: boolean }) => {
return useQuery({
queryKey: [NAME_SPACE, 'app-access-mode', appId],
queryFn: () => getAppAccessMode(appId!, isInstalledApp),
enabled: !!appId,
staleTime: 0,
gcTime: 0,
})
}
export const useGetUserCanAccessApp = ({ appId, isInstalledApp = true, enabled = true }: { appId?: string; isInstalledApp?: boolean; enabled?: boolean }) => {
return useQuery({
queryKey: [NAME_SPACE, 'user-can-access-app', appId],
queryFn: () => getUserCanAccess(appId!, isInstalledApp),
enabled: !!appId && enabled,
staleTime: 0,
gcTime: 0,
})
}

View File

@ -1,6 +1,6 @@
import type { Fetcher } from 'swr' import type { Fetcher } from 'swr'
import { del, get, patch, post, put } from './base' import { del, get, patch, post, put } from './base'
import type { ApiKeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppDetailResponse, AppListResponse, AppSSOResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, DSLImportMode, DSLImportResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app' import type { ApiKeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, DSLImportMode, DSLImportResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app'
import type { CommonResponse } from '@/models/common' import type { CommonResponse } from '@/models/common'
import type { AppIconType, AppMode, ModelConfig } from '@/types/app' import type { AppIconType, AppMode, ModelConfig } from '@/types/app'
import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type' import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type'
@ -13,13 +13,6 @@ export const fetchAppDetail = ({ url, id }: { url: string; id: string }) => {
return get<AppDetailResponse>(`${url}/${id}`) return get<AppDetailResponse>(`${url}/${id}`)
} }
export const fetchAppSSO = async ({ appId }: { appId: string }) => {
return get<AppSSOResponse>(`/enterprise/app-setting/sso?appID=${appId}`)
}
export const updateAppSSO = async ({ id, enabled }: { id: string; enabled: boolean }) => {
return post('/enterprise/app-setting/sso', { body: { app_id: id, enabled } })
}
export const fetchAppTemplates: Fetcher<AppTemplatesResponse, { url: string }> = ({ url }) => { export const fetchAppTemplates: Fetcher<AppTemplatesResponse, { url: string }> = ({ url }) => {
return get<AppTemplatesResponse>(url) return get<AppTemplatesResponse>(url)
} }

View File

@ -512,6 +512,15 @@ export const ssePost = (
}).catch(() => { }).catch(() => {
res.json().then((data: any) => { res.json().then((data: any) => {
if (isPublicAPI) { if (isPublicAPI) {
if (data.code === 'web_app_access_denied') {
Toast.notify({
type: 'error',
message: data.message,
})
setTimeout(() => {
requiredWebSSOLogin()
}, 1500)
}
if (data.code === 'web_sso_auth_required') if (data.code === 'web_sso_auth_required')
requiredWebSSOLogin() requiredWebSSOLogin()
@ -566,6 +575,16 @@ export const request = async<T>(url: string, options = {}, otherOptions?: IOther
// special code // special code
const { code, message } = errRespData const { code, message } = errRespData
// webapp sso // webapp sso
if (code === 'web_app_access_denied') {
Toast.notify({
type: 'error',
message,
})
setTimeout(() => {
requiredWebSSOLogin()
}, 1500)
return Promise.reject(err)
}
if (code === 'web_sso_auth_required') { if (code === 'web_sso_auth_required') {
requiredWebSSOLogin() requiredWebSSOLogin()
return Promise.reject(err) return Promise.reject(err)

View File

@ -11,7 +11,7 @@ import type {
ConversationItem, ConversationItem,
} from '@/models/share' } from '@/models/share'
import type { ChatConfig } from '@/app/components/base/chat/types' import type { ChatConfig } from '@/app/components/base/chat/types'
import type { SystemFeatures } from '@/types/feature' import type { AccessMode } from '@/models/access-control'
function getAction(action: 'get' | 'post' | 'del' | 'patch', isInstalledApp: boolean) { function getAction(action: 'get' | 'post' | 'del' | 'patch', isInstalledApp: boolean) {
switch (action) { switch (action) {
@ -144,10 +144,6 @@ export const fetchAppParams = async (isInstalledApp: boolean, installedAppId = '
return (getAction('get', isInstalledApp))(getUrl('parameters', isInstalledApp, installedAppId)) as Promise<ChatConfig> return (getAction('get', isInstalledApp))(getUrl('parameters', isInstalledApp, installedAppId)) as Promise<ChatConfig>
} }
export const fetchSystemFeatures = async () => {
return (getAction('get', false))(getUrl('system-features', false, '')) as Promise<SystemFeatures>
}
export const fetchWebSAMLSSOUrl = async (appCode: string, redirectUrl: string) => { export const fetchWebSAMLSSOUrl = async (appCode: string, redirectUrl: string) => {
return (getAction('get', false))(getUrl('/enterprise/sso/saml/login', false, ''), { return (getAction('get', false))(getUrl('/enterprise/sso/saml/login', false, ''), {
params: { params: {
@ -225,3 +221,17 @@ export const fetchAccessToken = async (appCode: string) => {
headers.append('X-App-Code', appCode) headers.append('X-App-Code', appCode)
return get('/passport', { headers }) as Promise<{ access_token: string }> return get('/passport', { headers }) as Promise<{ access_token: string }>
} }
export const getAppAccessMode = (appId: string, isInstalledApp: boolean) => {
if (isInstalledApp)
return consoleGet<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`)
return get<{ accessMode: AccessMode }>(`/webapp/access-mode?appId=${appId}`)
}
export const getUserCanAccess = (appId: string, isInstalledApp: boolean) => {
if (isInstalledApp)
return consoleGet<{ result: boolean }>(`/enterprise/webapp/permission?appId=${appId}`)
return get<{ result: boolean }>(`/webapp/permission?appId=${appId}`)
}

View File

@ -107,6 +107,7 @@ const config = {
'dataset-option-card-purple-gradient': 'var(--color-dataset-option-card-purple-gradient)', 'dataset-option-card-purple-gradient': 'var(--color-dataset-option-card-purple-gradient)',
'dataset-option-card-orange-gradient': 'var(--color-dataset-option-card-orange-gradient)', 'dataset-option-card-orange-gradient': 'var(--color-dataset-option-card-orange-gradient)',
'dataset-chunk-list-mask-bg': 'var(--color-dataset-chunk-list-mask-bg)', 'dataset-chunk-list-mask-bg': 'var(--color-dataset-chunk-list-mask-bg)',
'access-app-icon-mask-bg': 'var(--color-access-app-icon-mask-bg)',
}, },
lineClamp: { lineClamp: {
'20': '20', '20': '20',

View File

@ -1,24 +1,16 @@
html[data-theme="dark"] { html[data-theme="dark"] {
--color-chatbot-bg: linear-gradient( --color-chatbot-bg: linear-gradient(180deg,
180deg,
rgba(34, 34, 37, 0.9) 0%, rgba(34, 34, 37, 0.9) 0%,
rgba(29, 29, 32, 0.9) 90.48% rgba(29, 29, 32, 0.9) 90.48%);
); --color-chat-bubble-bg: linear-gradient(180deg,
--color-chat-bubble-bg: linear-gradient(
180deg,
rgba(200, 206, 218, 0.08) 0%, rgba(200, 206, 218, 0.08) 0%,
rgba(200, 206, 218, 0.02) 100% rgba(200, 206, 218, 0.02) 100%);
); --color-workflow-process-bg: linear-gradient(90deg,
--color-workflow-process-bg: linear-gradient(
90deg,
rgba(24, 24, 27, 0.25) 0%, rgba(24, 24, 27, 0.25) 0%,
rgba(24, 24, 27, 0.04) 100% rgba(24, 24, 27, 0.04) 100%);
); --color-account-teams-bg: linear-gradient(271deg,
--color-account-teams-bg: linear-gradient(
271deg,
rgba(34, 34, 37, 0.9) -0.1%, rgba(34, 34, 37, 0.9) -0.1%,
rgba(29, 29, 32, 0.9) 98.26% rgba(29, 29, 32, 0.9) 98.26%);
);
--color-dataset-chunk-process-success-bg: linear-gradient(92deg, rgba(23, 178, 106, 0.30) 0%, rgba(0, 0, 0, 0.00) 100%); --color-dataset-chunk-process-success-bg: linear-gradient(92deg, rgba(23, 178, 106, 0.30) 0%, rgba(0, 0, 0, 0.00) 100%);
--color-dataset-chunk-process-error-bg: linear-gradient(92deg, rgba(240, 68, 56, 0.30) 0%, rgba(0, 0, 0, 0.00) 100%); --color-dataset-chunk-process-error-bg: linear-gradient(92deg, rgba(240, 68, 56, 0.30) 0%, rgba(0, 0, 0, 0.00) 100%);
--color-dataset-chunk-detail-card-hover-bg: linear-gradient(180deg, #1D1D20 0%, #222225 100%); --color-dataset-chunk-detail-card-hover-bg: linear-gradient(180deg, #1D1D20 0%, #222225 100%);
@ -27,9 +19,8 @@ html[data-theme="dark"] {
--color-dataset-option-card-purple-gradient: linear-gradient(90deg, #25242E 0%, #1E1E21 100%); --color-dataset-option-card-purple-gradient: linear-gradient(90deg, #25242E 0%, #1E1E21 100%);
--color-dataset-option-card-orange-gradient: linear-gradient(90deg, #2B2322 0%, #1E1E21 100%); --color-dataset-option-card-orange-gradient: linear-gradient(90deg, #2B2322 0%, #1E1E21 100%);
--color-dataset-chunk-list-mask-bg: linear-gradient(180deg, rgba(34, 34, 37, 0.00) 0%, #222225 100%); --color-dataset-chunk-list-mask-bg: linear-gradient(180deg, rgba(34, 34, 37, 0.00) 0%, #222225 100%);
--mask-top2bottom-gray-50-to-transparent: linear-gradient( --mask-top2bottom-gray-50-to-transparent: linear-gradient(180deg,
180deg,
rgba(24, 24, 27, 0.08) 0%, rgba(24, 24, 27, 0.08) 0%,
rgba(0, 0, 0, 0) 100% rgba(0, 0, 0, 0) 100%);
); --color-access-app-icon-mask-bg: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.03) 100%);
} }

View File

@ -1,24 +1,16 @@
html[data-theme="light"] { html[data-theme="light"] {
--color-chatbot-bg: linear-gradient( --color-chatbot-bg: linear-gradient(180deg,
180deg,
rgba(249, 250, 251, 0.9) 0%, rgba(249, 250, 251, 0.9) 0%,
rgba(242, 244, 247, 0.9) 90.48% rgba(242, 244, 247, 0.9) 90.48%);
); --color-chat-bubble-bg: linear-gradient(180deg,
--color-chat-bubble-bg: linear-gradient(
180deg,
#fff 0%, #fff 0%,
rgba(255, 255, 255, 0.6) 100% rgba(255, 255, 255, 0.6) 100%);
); --color-workflow-process-bg: linear-gradient(90deg,
--color-workflow-process-bg: linear-gradient(
90deg,
rgba(200, 206, 218, 0.2) 0%, rgba(200, 206, 218, 0.2) 0%,
rgba(200, 206, 218, 0.04) 100% rgba(200, 206, 218, 0.04) 100%);
); --color-account-teams-bg: linear-gradient(271deg,
--color-account-teams-bg: linear-gradient(
271deg,
rgba(249, 250, 251, 0.9) -0.1%, rgba(249, 250, 251, 0.9) -0.1%,
rgba(242, 244, 247, 0.9) 98.26% rgba(242, 244, 247, 0.9) 98.26%);
);
--color-dataset-chunk-process-success-bg: linear-gradient(92deg, rgba(23, 178, 106, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%); --color-dataset-chunk-process-success-bg: linear-gradient(92deg, rgba(23, 178, 106, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%);
--color-dataset-chunk-process-error-bg: linear-gradient(92deg, rgba(240, 68, 56, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%); --color-dataset-chunk-process-error-bg: linear-gradient(92deg, rgba(240, 68, 56, 0.25) 0%, rgba(255, 255, 255, 0.00) 100%);
--color-dataset-chunk-detail-card-hover-bg: linear-gradient(180deg, #F2F4F7 0%, #F9FAFB 100%); --color-dataset-chunk-detail-card-hover-bg: linear-gradient(180deg, #F2F4F7 0%, #F9FAFB 100%);
@ -27,9 +19,8 @@ html[data-theme="light"] {
--color-dataset-option-card-purple-gradient: linear-gradient(90deg, #F0EEFA 0%, #F9FAFB 100%); --color-dataset-option-card-purple-gradient: linear-gradient(90deg, #F0EEFA 0%, #F9FAFB 100%);
--color-dataset-option-card-orange-gradient: linear-gradient(90deg, #F8F2EE 0%, #F9FAFB 100%); --color-dataset-option-card-orange-gradient: linear-gradient(90deg, #F8F2EE 0%, #F9FAFB 100%);
--color-dataset-chunk-list-mask-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FCFCFD 100%); --color-dataset-chunk-list-mask-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FCFCFD 100%);
--mask-top2bottom-gray-50-to-transparent: linear-gradient( --mask-top2bottom-gray-50-to-transparent: linear-gradient(180deg,
180deg,
rgba(200, 206, 218, 0.2) 0%, rgba(200, 206, 218, 0.2) 0%,
rgba(255, 255, 255, 0) 100% rgba(255, 255, 255, 0) 100%);
); --color-access-app-icon-mask-bg: linear-gradient(135deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.08) 100%);
} }

View File

@ -7,6 +7,7 @@ import type {
WeightedScoreEnum, WeightedScoreEnum,
} from '@/models/datasets' } from '@/models/datasets'
import type { UploadFileSetting } from '@/app/components/workflow/types' import type { UploadFileSetting } from '@/app/components/workflow/types'
import type { AccessMode } from '@/models/access-control'
export enum Theme { export enum Theme {
light = 'light', light = 'light',
@ -351,6 +352,8 @@ export type App = {
/** api site url */ /** api site url */
api_base_url: string api_base_url: string
tags: Tag[] tags: Tag[]
/** access control */
access_mode: AccessMode
} }
export type AppSSO = { export type AppSSO = {

Some files were not shown because too many files have changed in this diff Show More