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

View File

@ -8,19 +8,16 @@ import Loading from '@/app/components/base/loading'
import { ToastContext } from '@/app/components/base/toast'
import {
fetchAppDetail,
fetchAppSSO,
updateAppSSO,
updateAppSiteAccessToken,
updateAppSiteConfig,
updateAppSiteStatus,
} from '@/service/apps'
import type { App, AppSSO } from '@/types/app'
import type { App } from '@/types/app'
import type { UpdateAppSiteCodeResponse } from '@/models/app'
import { asyncRunSafe } from '@/utils'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import type { IAppCardProps } from '@/app/components/app/overview/appCard'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type ICardViewProps = {
appId: string
@ -31,18 +28,11 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
const { notify } = useContext(ToastContext)
const appDetail = useAppStore(state => state.appDetail)
const setAppDetail = useAppStore(state => state.setAppDetail)
const { systemFeatures } = useGlobalPublicStore()
const updateAppDetail = async () => {
try {
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) }
}
@ -93,16 +83,6 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
if (!err)
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)
}

View File

@ -4,7 +4,7 @@ import { useContext, useContextSelector } from 'use-context-selector'
import { useRouter } from 'next/navigation'
import { useCallback, useEffect, useState } from 'react'
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 cn from '@/utils/classnames'
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 { fetchInstalledAppList } from '@/service/explore'
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 = {
app: App
@ -53,6 +56,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
const [showDuplicateModal, setShowDuplicateModal] = useState(false)
const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [showAccessControl, setShowAccessControl] = useState(false)
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
const onConfirmDelete = useCallback(async () => {
@ -71,7 +75,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
})
}
setShowConfirmDelete(false)
}, [app.id])
}, [app.id, mutateApps, notify, onPlanInfoChanged, onRefresh, t])
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
name,
@ -175,6 +179,13 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
setShowSwitchModal(false)
}
const onUpdateAccessControl = useCallback(() => {
if (onRefresh)
onRefresh()
mutateApps()
setShowAccessControl(false)
}, [onRefresh, mutateApps, setShowAccessControl])
const Operations = (props: HtmlContentProps) => {
const onMouseLeave = async () => {
props.onClose?.()
@ -209,6 +220,12 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
e.preventDefault()
setShowConfirmDelete(true)
}
const onClickAccessControl = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
props.onClick?.()
e.preventDefault()
setShowAccessControl(true)
}
const onClickInstalledApp = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
props.onClick?.()
@ -252,6 +269,14 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
<span className={s.actionName}>{t('app.openInExplore')}</span>
</button>
<Divider className="!my-1" />
{
isCurrentWorkspaceEditor && <>
<button className={s.actionItem} onClick={onClickAccessControl}>
<span className={s.actionName}>{t('app.accessControl')}</span>
</button>
<Divider />
</>
}
<div
className={cn(s.actionItem, s.deleteActionItem, 'group')}
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'
>
<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'>
<AppIcon
size="large"
@ -301,6 +326,17 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
{app.mode === 'completion' && <div className='truncate'>{t('app.types.completion').toUpperCase()}</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 className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-text-tertiary'>
<div
@ -357,7 +393,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
popupClassName={
(app.mode === 'completion' || app.mode === 'chat')
? '!w-[256px] translate-x-[-224px]'
: '!w-[160px] translate-x-[-128px]'
: '!w-[216px] translate-x-[-128px]'
}
className={'h-fit !z-20'}
/>
@ -418,6 +454,9 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
onClose={() => setSecretEnvList([])}
/>
)}
{showAccessControl && (
<AccessControl app={app} onConfirm={onUpdateAccessControl} onClose={() => setShowAccessControl(false)} />
)}
</>
)
}

View File

@ -1,14 +1,21 @@
'use client'
import { useRouter, useSearchParams } from 'next/navigation'
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 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 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'
const WebSSOForm: FC = () => {
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const searchParams = useSearchParams()
const router = useRouter()
@ -23,15 +30,15 @@ const WebSSOForm: FC = () => {
})
}
const getAppCodeFromRedirectUrl = () => {
const getAppCodeFromRedirectUrl = useCallback(() => {
const appCode = redirectUrl?.split('/').pop()
if (!appCode)
return null
return appCode
}
}, [redirectUrl])
const processTokenAndRedirect = async () => {
const processTokenAndRedirect = useCallback(async () => {
const appCode = getAppCodeFromRedirectUrl()
if (!appCode || !tokenFromUrl || !redirectUrl) {
showErrorToast('redirect url or app code or token is invalid.')
@ -40,27 +47,27 @@ const WebSSOForm: FC = () => {
await setAccessToken(appCode, tokenFromUrl)
router.push(redirectUrl)
}
}, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl])
const handleSSOLogin = async (protocol: string) => {
const handleSSOLogin = async () => {
const appCode = getAppCodeFromRedirectUrl()
if (!appCode || !redirectUrl) {
showErrorToast('redirect url or app code is invalid.')
return
}
switch (protocol) {
case 'saml': {
switch (systemFeatures.webapp_auth.sso_config.protocol) {
case SSOProtocol.SAML: {
const samlRes = await fetchWebSAMLSSOUrl(appCode, redirectUrl)
router.push(samlRes.url)
break
}
case 'oidc': {
case SSOProtocol.OIDC: {
const oidcRes = await fetchWebOIDCSSOUrl(appCode, redirectUrl)
router.push(oidcRes.url)
break
}
case 'oauth2': {
case SSOProtocol.OAuth2: {
const oauth2Res = await fetchWebOAuth2SSOUrl(appCode, redirectUrl)
router.push(oauth2Res.url)
break
@ -72,32 +79,52 @@ const WebSSOForm: FC = () => {
useEffect(() => {
const init = async () => {
const res = await fetchSystemFeatures()
const protocol = res.sso_enforced_for_web_protocol
if (message) {
showErrorToast(message)
return
}
if (!tokenFromUrl) {
await handleSSOLogin(protocol)
if (!tokenFromUrl)
return
}
await processTokenAndRedirect()
}
init()
}, [message, tokenFromUrl]) // Added dependencies to useEffect
}, [message, processTokenAndRedirect, tokenFromUrl])
if (tokenFromUrl)
return <div className='flex items-center justify-center h-full'><Loading /></div>
return (
<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]')}>
<Loading type='area' />
if (systemFeatures.webapp_auth.enabled) {
if (systemFeatures.webapp_auth.allow_sso) {
return (
<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]')}>
<Button variant='primary' onClick={() => { handleSSOLogin() }}>{t('login.withSSO')}</Button>
</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)

View File

@ -5,6 +5,7 @@ import { RiArrowDownSLine } from '@remixicon/react'
import React, { useCallback, useState } from 'react'
import AppIcon from '../base/app-icon'
import SwitchAppModal from '../app/switch-app-modal'
import AccessControl from '../app/app-access-control'
import s from './style.module.css'
import cn from '@/utils/classnames'
import {
@ -18,7 +19,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
import { ToastContext } from '@/app/components/base/toast'
import AppsContext, { useAppContext } from '@/context/app-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 type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
import CreateAppModal from '@/app/components/explore/create-app-modal'
@ -50,6 +51,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
const [showSwitchTip, setShowSwitchTip] = useState<string>('')
const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
const [showImportDSLModal, setShowImportDSLModal] = useState<boolean>(false)
const [showAccessControl, setShowAccessControl] = useState<boolean>(false)
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
const mutateApps = useContextSelector(
@ -175,7 +177,20 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
})
}
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()
@ -374,6 +389,10 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
</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" />
<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)
@ -466,6 +485,11 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
onClose={() => setSecretEnvList([])}
/>
)}
{
showAccessControl && <AccessControl app={appDetail}
onConfirm={handleAccessControlUpdate}
onClose={() => { setShowAccessControl(false) }} />
}
</div>
</PortalToFollowElem>
)

View File

@ -17,7 +17,7 @@ export type IAppDetailNavProps = {
desc: string
isExternal?: boolean
icon: string
icon_background: string
icon_background: string | null
navigation: Array<{
name: 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 {
memo,
useCallback,
useEffect,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import dayjs from 'dayjs'
import { RiArrowDownSLine, RiPlanetLine } from '@remixicon/react'
import { RiArrowDownSLine, RiArrowRightSLine, RiLockLine, RiPlanetLine } from '@remixicon/react'
import Toast from '../../base/toast'
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 PublishWithMultipleModel from './publish-with-multiple-model'
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 type { InputVar } from '@/app/components/workflow/types'
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 = {
disabled?: boolean
@ -65,10 +73,31 @@ const AppPublisher = ({
const [published, setPublished] = useState(false)
const [open, setOpen] = useState(false)
const appDetail = useAppStore(state => state.appDetail)
const setAppDetail = useAppStore(s => s.setAppDetail)
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
const appMode = (appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow') ? 'chat' : appDetail.mode
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 formatTimeFromNow = useCallback((time: number) => {
return dayjs(time).locale(language === 'zh_Hans' ? 'zh-cn' : language.replace('_', '-')).fromNow()
@ -120,6 +149,13 @@ const AppPublisher = ({
}
}, [appDetail?.id])
const handleAccessControlUpdate = useCallback(() => {
fetchAppDetail({ url: '/apps', id: appDetail!.id }).then((res) => {
setAppDetail(res)
setShowAppAccessControl(false)
})
}, [appDetail, setAppDetail])
const [embeddingModalOpen, setEmbeddingModalOpen] = useState(false)
return (
@ -196,58 +232,95 @@ const AppPublisher = ({
)
}
</div>
<div className='p-4 pt-3 border-t-[0.5px] border-t-black/5'>
<SuggestedAction disabled={!publishedAt} link={appURL} icon={<PlayCircle />}>{t('workflow.common.runApp')}</SuggestedAction>
{appDetail?.mode === 'workflow'
? (
<SuggestedAction
disabled={!publishedAt}
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
icon={<LeftIndent02 className='w-4 h-4' />}
>
{t('workflow.common.batchRunApp')}
</SuggestedAction>
)
: (
<SuggestedAction
{(isGettingUserCanAccessApp || isGettingAppWhiteListSubjects)
? <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={() => {
setEmbeddingModalOpen(true)
handleTrigger()
}}
disabled={!publishedAt}
icon={<CodeBrowser className='w-4 h-4' />}
>
{t('workflow.common.embedIntoSite')}
</SuggestedAction>
)}
<SuggestedAction
onClick={() => {
handleOpenInExplore()
}}
disabled={!publishedAt}
icon={<RiPlanetLine className='w-4 h-4' />}
>
{t('workflow.common.openInExplore')}
</SuggestedAction>
<SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction>
{appDetail?.mode === 'workflow' && (
<WorkflowToolConfigureButton
disabled={!publishedAt}
published={!!toolPublished}
detailNeedUpdate={!!toolPublished && published}
workflowAppId={appDetail?.id}
icon={{
content: (appDetail.icon_type === 'image' ? '🤖' : appDetail?.icon) || '🤖',
background: (appDetail.icon_type === 'image' ? appDefaultIconBackground : appDetail?.icon_background) || appDefaultIconBackground,
}}
name={appDetail?.name}
description={appDetail?.description}
inputs={inputs}
handlePublish={handlePublish}
onRefreshData={onRefreshData}
/>
)}
</div>
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'
? (<div className='flex'>
<SuggestedAction
disabled={!publishedAt}
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
icon={<LeftIndent02 className='w-4 h-4' />}
>
{t('workflow.common.batchRunApp')}
</SuggestedAction>
</div>
)
: (<div className='flex'>
<SuggestedAction
onClick={() => {
setEmbeddingModalOpen(true)
handleTrigger()
}}
disabled={!publishedAt}
icon={<CodeBrowser className='w-4 h-4' />}
>
{t('workflow.common.embedIntoSite')}
</SuggestedAction>
</div>
)}
<Tooltip triggerClassName='flex' disabled={useCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}>
<SuggestedAction
onClick={() => {
handleOpenInExplore()
}}
disabled={!publishedAt || !useCanAccessApp?.result}
icon={<RiPlanetLine className='w-4 h-4' />}
>
{t('workflow.common.openInExplore')}
</SuggestedAction>
</Tooltip>
<div className='flex' >
<SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction>
</div>
{appDetail?.mode === 'workflow' && (
<div className='flex' >
<WorkflowToolConfigureButton
disabled={!publishedAt}
published={!!toolPublished}
detailNeedUpdate={!!toolPublished && published}
workflowAppId={appDetail?.id}
icon={{
content: (appDetail.icon_type === 'image' ? '🤖' : appDetail?.icon) || '🤖',
background: (appDetail.icon_type === 'image' ? appDefaultIconBackground : appDetail?.icon_background) || appDefaultIconBackground,
}}
name={appDetail?.name}
description={appDetail?.description}
inputs={inputs}
handlePublish={handlePublish}
onRefreshData={onRefreshData}
/>
</div>
)}
</div>
</>}
</div>
</PortalToFollowElemContent>
<EmbeddedModal
@ -257,6 +330,7 @@ const AppPublisher = ({
appBaseUrl={appBaseURL}
accessToken={accessToken}
/>
{showAppAccessControl && <AccessControl app={appDetail!} onConfirm={handleAccessControlUpdate} onClose={() => { setShowAppAccessControl(false) }} />}
</PortalToFollowElem >
)
}

View File

@ -8,22 +8,30 @@ export type SuggestedActionProps = PropsWithChildren<HTMLProps<HTMLAnchorElement
disabled?: boolean
}>
const SuggestedAction = ({ icon, link, disabled, children, className, ...props }: SuggestedActionProps) => (
<a
href={disabled ? undefined : link}
target='_blank'
rel='noreferrer'
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',
disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'hover:bg-primary-50 hover:text-primary-600 cursor-pointer',
className,
)}
{...props}
>
<div className='relative w-4 h-4'>{icon}</div>
<div className='grow shrink basis-0 text-[13px] font-medium leading-[18px]'>{children}</div>
<ArrowUpRight />
</a>
)
const SuggestedAction = ({ icon, link, disabled, children, className, onClick, ...props }: SuggestedActionProps) => {
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
if (disabled)
return
onClick?.(e)
}
return (
<a
href={disabled ? undefined : link}
target='_blank'
rel='noreferrer'
className={classNames(
'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',
className,
)}
onClick={handleClick}
{...props}
>
<div className='relative w-4 h-4'>{icon}</div>
<div className='grow shrink basis-0 text-[13px] font-medium leading-[18px]'>{children}</div>
<ArrowUpRight />
</a>
)
}
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 { LanguagesSupported, languages } from '@/i18n/language'
import Tooltip from '@/app/components/base/tooltip'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context'
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import AppIconPicker from '@/app/components/base/app-icon-picker'
import I18n from '@/context/i18n'
import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type ISettingsModalProps = {
isChat: boolean
@ -66,8 +64,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
onClose,
onSave,
}) => {
const { systemFeatures } = useGlobalPublicStore()
const { isCurrentWorkspaceEditor } = useAppContext()
const { notify } = useToastContext()
const [isShowMore, setIsShowMore] = useState(false)
const {
@ -139,7 +135,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
setAppIcon(icon_type === 'image'
? { type: 'image', url: icon_url!, fileId: icon }
: { 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 = () => {
onClose()
@ -325,28 +321,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
</div>
<p className='pb-0.5 text-text-tertiary body-xs-regular'>{t(`${prefixSettings}.workflow.showDesc`)}</p>
</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 */}
<Divider className="h-px my-0" />
{!isShowMore && (

View File

@ -17,7 +17,7 @@ const AppUnavailable: FC<IAppUnavailableProps> = ({
const { t } = useTranslation()
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'
style={{
borderRight: '1px solid rgba(0,0,0,.3)',

View File

@ -15,12 +15,15 @@ import type {
AppMeta,
ConversationItem,
} from '@/models/share'
import { AccessMode } from '@/models/access-control'
export type ChatWithHistoryContextValue = {
appInfoError?: any
appInfoLoading?: boolean
appMeta?: AppMeta
appData?: AppData
accessMode?: AccessMode
userCanAccess?: boolean
appParams?: ChatConfig
appChatListDataLoading?: boolean
currentConversationId: string
@ -52,6 +55,8 @@ export type ChatWithHistoryContextValue = {
}
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
userCanAccess: false,
currentConversationId: '',
appPrevChatTree: [],
pinnedConversationList: [],
@ -59,21 +64,21 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
showConfigPanelBeforeChat: false,
newConversationInputs: {},
newConversationInputsRef: { current: {} },
handleNewConversationInputsChange: () => {},
handleNewConversationInputsChange: () => { },
inputsForms: [],
handleNewConversation: () => {},
handleStartChat: () => {},
handleChangeConversation: () => {},
handlePinConversation: () => {},
handleUnpinConversation: () => {},
handleDeleteConversation: () => {},
handleNewConversation: () => { },
handleStartChat: () => { },
handleChangeConversation: () => { },
handlePinConversation: () => { },
handleUnpinConversation: () => { },
handleDeleteConversation: () => { },
conversationRenaming: false,
handleRenameConversation: () => {},
handleNewConversationCompleted: () => {},
handleRenameConversation: () => { },
handleNewConversationCompleted: () => { },
chatShouldReloadKey: '',
isMobile: false,
isInstalledApp: false,
handleFeedback: () => {},
currentChatInstanceRef: { current: { handleStop: () => {} } },
handleFeedback: () => { },
currentChatInstanceRef: { current: { handleStop: () => { } } },
})
export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)

View File

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

View File

@ -27,6 +27,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
className,
}) => {
const {
userCanAccess,
appInfoError,
appData,
appInfoLoading,
@ -57,6 +58,8 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
<Loading type='app' />
)
}
if (!userCanAccess)
return <AppUnavailable code={403} unknownReason='no permission.' />
if (appInfoError) {
return (
@ -114,6 +117,8 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
const {
appInfoError,
appInfoLoading,
accessMode,
userCanAccess,
appData,
appParams,
appMeta,
@ -149,6 +154,8 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
appInfoError,
appInfoLoading,
appData,
accessMode,
userCanAccess,
appParams,
appMeta,
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 Confirm from '@/app/components/base/confirm'
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 { t } = useTranslation()
const {
isInstalledApp,
accessMode,
appData,
pinnedConversationList,
conversationList,
@ -115,11 +119,14 @@ const Sidebar = () => {
)
}
</div>
{appData?.site.copyright && (
<div className='px-4 pb-4 text-xs text-gray-400'>
© {(new Date()).getFullYear()} {appData?.site.copyright}
</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 && (
<div className='text-xs text-gray-400 truncate'>
© {(new Date()).getFullYear()} {appData?.site.copyright}
</div>
)}
</div>
{!!showConfirm && (
<Confirm
title={t('share.chat.deleteConversation.title')}

View File

@ -14,8 +14,11 @@ import type {
AppMeta,
ConversationItem,
} from '@/models/share'
import { AccessMode } from '@/models/access-control'
export type EmbeddedChatbotContextValue = {
accessMode?: AccessMode
userCanAccess?: boolean
appInfoError?: any
appInfoLoading?: boolean
appMeta?: AppMeta
@ -46,6 +49,8 @@ export type EmbeddedChatbotContextValue = {
}
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
userCanAccess: false,
accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
currentConversationId: '',
appPrevChatList: [],
pinnedConversationList: [],
@ -53,16 +58,16 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>
showConfigPanelBeforeChat: false,
newConversationInputs: {},
newConversationInputsRef: { current: {} },
handleNewConversationInputsChange: () => {},
handleNewConversationInputsChange: () => { },
inputsForms: [],
handleNewConversation: () => {},
handleStartChat: () => {},
handleChangeConversation: () => {},
handleNewConversationCompleted: () => {},
handleNewConversation: () => { },
handleStartChat: () => { },
handleChangeConversation: () => { },
handleNewConversationCompleted: () => { },
chatShouldReloadKey: '',
isMobile: false,
isInstalledApp: false,
handleFeedback: () => {},
currentChatInstanceRef: { current: { handleStop: () => {} } },
handleFeedback: () => { },
currentChatInstanceRef: { current: { handleStop: () => { } } },
})
export const useEmbeddedChatbotContext = () => useContext(EmbeddedChatbotContext)

View File

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

View File

@ -26,6 +26,7 @@ import Tooltip from '@/app/components/base/tooltip'
const Chatbot = () => {
const { t } = useTranslation()
const {
userCanAccess,
isMobile,
appInfoError,
appInfoLoading,
@ -59,6 +60,9 @@ const Chatbot = () => {
)
}
if (!userCanAccess)
return <AppUnavailable code={403} unknownReason='no permission.' />
if (appInfoError) {
return (
<AppUnavailable />
@ -91,7 +95,7 @@ const Chatbot = () => {
popupContent={t('share.chat.resetChat')}
>
<div className='p-1.5 bg-white border-[0.5px] border-gray-100 rounded-lg shadow-md cursor-pointer' onClick={handleNewConversation}>
<RiLoopLeftLine className="h-4 w-4 text-gray-500"/>
<RiLoopLeftLine className="h-4 w-4 text-gray-500" />
</div>
</Tooltip>
</div>
@ -114,6 +118,8 @@ const EmbeddedChatbotWrapper = () => {
appInfoError,
appInfoLoading,
appData,
accessMode,
userCanAccess,
appParams,
appMeta,
appChatListDataLoading,
@ -139,6 +145,8 @@ const EmbeddedChatbotWrapper = () => {
} = useEmbeddedChatbot()
return <EmbeddedChatbotContext.Provider value={{
userCanAccess,
accessMode,
appInfoError,
appInfoLoading,
appData,

View File

@ -90,6 +90,7 @@ const Tooltip: FC<TooltipProps> = ({
}}
onMouseLeave={() => triggerMethod === 'hover' && handleLeave(true)}
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>}
</PortalToFollowElemTrigger>

View File

@ -18,12 +18,12 @@ import { NotionPageSelector } from '@/app/components/base/notion-page-selector'
import { useDatasetDetailContext } from '@/context/dataset-detail'
import { useProviderContext } from '@/context/provider-context'
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 = {
datasetId?: string
dataSourceType?: DataSourceType
dataSourceTypeDisable: Boolean
dataSourceTypeDisable: boolean
hasConnection: boolean
onSetting: () => void
files: FileItem[]
@ -44,14 +44,20 @@ type IStepOneProps = {
type NotionConnectorProps = {
onSetting: () => void
}
export const NotionConnector = ({ onSetting }: NotionConnectorProps) => {
export const NotionConnector = (props: NotionConnectorProps) => {
const { onSetting } = props
const { t } = useTranslation()
return (
<div className={s.notionConnectionTip}>
<span className={s.notionIcon} />
<div className={s.title}>{t('datasetCreation.stepOne.notionSyncTitle')}</div>
<div className={s.tip}>{t('datasetCreation.stepOne.notionSyncTip')}</div>
<div className='flex w-[640px] flex-col items-start rounded-2xl bg-workflow-process-bg p-6'>
<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='mb-1 flex flex-col gap-y-1 pb-3 pt-1'>
<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>
</div>
)
@ -120,175 +126,195 @@ const StepOne = ({
return true
if (files.some(file => !file.file.id))
return true
if (isShowVectorSpaceFull)
return true
return false
return isShowVectorSpaceFull
}, [files, isShowVectorSpaceFull])
return (
<div className='flex w-full h-full'>
<div className='w-1/2 h-full overflow-y-auto relative'>
<div className='flex justify-end'>
<div className={classNames(s.form)}>
{
shouldShowDataSourceTypeList && (
<div className={classNames(s.stepHeader, 'z-10 text-text-secondary bg-components-panel-bg-blur')}>{t('datasetCreation.steps.one')}</div>
)
}
{
shouldShowDataSourceTypeList && (
<div className='flex items-center mb-8 flex-wrap gap-4'>
<div
className={cn(
s.dataSourceItem,
dataSourceType === DataSourceType.FILE && s.active,
dataSourceTypeDisable && dataSourceType !== DataSourceType.FILE && s.disabled,
)}
onClick={() => {
if (dataSourceTypeDisable)
return
changeType(DataSourceType.FILE)
hideFilePreview()
hideNotionPagePreview()
}}
>
<span className={cn(s.datasetIcon)} />
{t('datasetCreation.stepOne.dataSourceType.file')}
<div className='h-full w-full overflow-x-auto'>
<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={cn(s.form)}>
{
shouldShowDataSourceTypeList && (
<div className={cn(s.stepHeader, 'text-text-secondary system-md-semibold')}>
{t('datasetCreation.steps.one')}
</div>
<div
className={cn(
s.dataSourceItem,
dataSourceType === DataSourceType.NOTION && s.active,
dataSourceTypeDisable && dataSourceType !== DataSourceType.NOTION && s.disabled,
)}
onClick={() => {
if (dataSourceTypeDisable)
return
changeType(DataSourceType.NOTION)
hideFilePreview()
hideNotionPagePreview()
}}
>
<span className={cn(s.datasetIcon, s.notion)} />
{t('datasetCreation.stepOne.dataSourceType.notion')}
</div>
<div
className={cn(
s.dataSourceItem,
dataSourceType === DataSourceType.WEB && s.active,
dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled,
)}
onClick={() => changeType(DataSourceType.WEB)}
>
<span className={cn(s.datasetIcon, s.web)} />
{t('datasetCreation.stepOne.dataSourceType.web')}
</div>
</div>
)
}
{dataSourceType === DataSourceType.FILE && (
<>
<FileUploader
fileList={files}
titleClassName={!shouldShowDataSourceTypeList ? 'mt-[30px] !mb-[44px] !text-lg !font-semibold !text-gray-900' : undefined}
prepareFileList={updateFileList}
onFileListUpdate={updateFileList}
onFileUpdate={updateFile}
onPreview={updateCurrentFile}
notSupportBatchUpload={notSupportBatchUpload}
/>
{isShowVectorSpaceFull && (
<div className='max-w-[640px] mb-4'>
<VectorSpaceFull />
</div>
)}
<div className="flex justify-end gap-2 max-w-[640px]">
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
<Button disabled={nextDisabled} variant='primary' onClick={onStepChange}>
<span className="flex gap-0.5 px-[10px]">
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
<RiArrowRightLine className="size-4" />
</span>
</Button>
</div>
</>
)}
{dataSourceType === DataSourceType.NOTION && (
<>
{!hasConnection && <NotionConnector onSetting={onSetting} />}
{hasConnection && (
<>
<div className='mb-8 w-[640px]'>
<NotionPageSelector
value={notionPages.map(page => page.page_id)}
onSelect={updateNotionPages}
onPreview={updateCurrentPage}
/>
)
}
{
shouldShowDataSourceTypeList && (
<div className='mb-8 grid grid-cols-3 gap-4'>
<div
className={cn(
s.dataSourceItem,
'system-sm-medium',
dataSourceType === DataSourceType.FILE && s.active,
dataSourceTypeDisable && dataSourceType !== DataSourceType.FILE && s.disabled,
)}
onClick={() => {
if (dataSourceTypeDisable)
return
changeType(DataSourceType.FILE)
hideFilePreview()
hideNotionPagePreview()
}}
>
<span className={cn(s.datasetIcon)} />
<span
title={t('datasetCreation.stepOne.dataSourceType.file')!}
className='truncate'
>
{t('datasetCreation.stepOne.dataSourceType.file')}
</span>
</div>
{isShowVectorSpaceFull && (
<div className='max-w-[640px] mb-4'>
<VectorSpaceFull />
</div>
)}
<div className="flex justify-end gap-2 max-w-[640px]">
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
<Button disabled={isShowVectorSpaceFull || !notionPages.length} variant='primary' onClick={onStepChange}>
<span className="flex gap-0.5 px-[10px]">
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
<RiArrowRightLine className="size-4" />
</span>
</Button>
<div
className={cn(
s.dataSourceItem,
'system-sm-medium',
dataSourceType === DataSourceType.NOTION && s.active,
dataSourceTypeDisable && dataSourceType !== DataSourceType.NOTION && s.disabled,
)}
onClick={() => {
if (dataSourceTypeDisable)
return
changeType(DataSourceType.NOTION)
hideFilePreview()
hideNotionPagePreview()
}}
>
<span className={cn(s.datasetIcon, s.notion)} />
<span
title={t('datasetCreation.stepOne.dataSourceType.notion')!}
className='truncate'
>
{t('datasetCreation.stepOne.dataSourceType.notion')}
</span>
</div>
</>
)}
</>
)}
{dataSourceType === DataSourceType.WEB && (
<>
<div className={cn('mb-8 w-[640px]', !shouldShowDataSourceTypeList && 'mt-12')}>
<Website
onPreview={setCurrentWebsite}
checkedCrawlResult={websitePages}
onCheckedCrawlResultChange={updateWebsitePages}
onCrawlProviderChange={onWebsiteCrawlProviderChange}
onJobIdChange={onWebsiteCrawlJobIdChange}
crawlOptions={crawlOptions}
onCrawlOptionsChange={onCrawlOptionsChange}
<div
className={cn(
s.dataSourceItem,
'system-sm-medium',
dataSourceType === DataSourceType.WEB && s.active,
dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled,
)}
onClick={() => changeType(DataSourceType.WEB)}
>
<span className={cn(s.datasetIcon, s.web)} />
<span
title={t('datasetCreation.stepOne.dataSourceType.web')!}
className='truncate'
>
{t('datasetCreation.stepOne.dataSourceType.web')}
</span>
</div>
</div>
)
}
{dataSourceType === DataSourceType.FILE && (
<>
<FileUploader
fileList={files}
titleClassName={!shouldShowDataSourceTypeList ? 'mt-[30px] !mb-[44px] !text-lg' : undefined}
prepareFileList={updateFileList}
onFileListUpdate={updateFileList}
onFileUpdate={updateFile}
onPreview={updateCurrentFile}
notSupportBatchUpload={notSupportBatchUpload}
/>
</div>
{isShowVectorSpaceFull && (
<div className='max-w-[640px] mb-4'>
<VectorSpaceFull />
{isShowVectorSpaceFull && (
<div className='mb-4 max-w-[640px]'>
<VectorSpaceFull />
</div>
)}
<div className="flex max-w-[640px] justify-end gap-2">
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
<Button disabled={nextDisabled} variant='primary' onClick={onStepChange}>
<span className="flex gap-0.5 px-[10px]">
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
<RiArrowRightLine className="size-4" />
</span>
</Button>
</div>
)}
<div className="flex justify-end gap-2 max-w-[640px]">
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
<Button disabled={isShowVectorSpaceFull || !websitePages.length} variant='primary' onClick={onStepChange}>
<span className="flex gap-0.5 px-[10px]">
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
<RiArrowRightLine className="size-4" />
</span>
</Button>
</div>
</>
)}
{!datasetId && (
<>
<div className={s.dividerLine} />
<span className="inline-flex items-center cursor-pointer text-[13px] leading-4 text-text-accent" onClick={modalShowHandle}>
<RiFolder6Line className="size-4 mr-1" />
{t('datasetCreation.stepOne.emptyDatasetCreation')}
</span>
</>
)}
</>
)}
{dataSourceType === DataSourceType.NOTION && (
<>
{!hasConnection && <NotionConnector onSetting={onSetting} />}
{hasConnection && (
<>
<div className='mb-8 w-[640px]'>
<NotionPageSelector
value={notionPages.map(page => page.page_id)}
onSelect={updateNotionPages}
onPreview={updateCurrentPage}
/>
</div>
{isShowVectorSpaceFull && (
<div className='mb-4 max-w-[640px]'>
<VectorSpaceFull />
</div>
)}
<div className="flex max-w-[640px] justify-end gap-2">
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
<Button disabled={isShowVectorSpaceFull || !notionPages.length} variant='primary' onClick={onStepChange}>
<span className="flex gap-0.5 px-[10px]">
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
<RiArrowRightLine className="size-4" />
</span>
</Button>
</div>
</>
)}
</>
)}
{dataSourceType === DataSourceType.WEB && (
<>
<div className={cn('mb-8 w-[640px]', !shouldShowDataSourceTypeList && 'mt-12')}>
<Website
onPreview={setCurrentWebsite}
checkedCrawlResult={websitePages}
onCheckedCrawlResultChange={updateWebsitePages}
onCrawlProviderChange={onWebsiteCrawlProviderChange}
onJobIdChange={onWebsiteCrawlJobIdChange}
crawlOptions={crawlOptions}
onCrawlOptionsChange={onCrawlOptionsChange}
/>
</div>
{isShowVectorSpaceFull && (
<div className='mb-4 max-w-[640px]'>
<VectorSpaceFull />
</div>
)}
<div className="flex max-w-[640px] justify-end gap-2">
{/* <Button>{t('datasetCreation.stepOne.cancel')}</Button> */}
<Button disabled={isShowVectorSpaceFull || !websitePages.length} variant='primary' onClick={onStepChange}>
<span className="flex gap-0.5 px-[10px]">
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
<RiArrowRightLine className="size-4" />
</span>
</Button>
</div>
</>
)}
{!datasetId && (
<>
<div className='my-8 h-px max-w-[640px] bg-divider-regular' />
<span className="inline-flex cursor-pointer items-center text-[13px] leading-4 text-text-accent" onClick={modalShowHandle}>
<RiFolder6Line className="mr-1 size-4" />
{t('datasetCreation.stepOne.emptyDatasetCreation')}
</span>
</>
)}
</div>
<EmptyDatasetCreationModal show={showModal} onHide={modalCloseHandle} />
</div>
<EmptyDatasetCreationModal show={showModal} onHide={modalCloseHandle} />
</div>
</div>
<div className='w-1/2 h-full overflow-y-auto'>
{currentFile && <FilePreview file={currentFile} hidePreview={hideFilePreview} />}
{currentNotionPage && <NotionPagePreview currentPage={currentNotionPage} hidePreview={hideNotionPagePreview} />}
{currentWebsite && <WebsitePreview payload={currentWebsite} hidePreview={hideWebsitePreview} />}
<div className='h-full w-1/2 overflow-y-auto'>
{currentFile && <FilePreview file={currentFile} hidePreview={hideFilePreview} />}
{currentNotionPage && <NotionPagePreview currentPage={currentNotionPage} hidePreview={hideNotionPagePreview} />}
{currentWebsite && <WebsitePreview payload={currentWebsite} hidePreview={hideWebsitePreview} />}
</div>
</div>
</div>
)

View File

@ -26,15 +26,15 @@ const InstalledApp: FC<IInstalledAppProps> = ({
}
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' && (
<ChatWithHistory installedAppInfo={installedApp} className='rounded-2xl shadow-md overflow-hidden' />
)}
{installedApp.app.mode === 'completion' && (
<TextGenerationApp isInstalledApp installedAppInfo={installedApp}/>
<TextGenerationApp isInstalledApp installedAppInfo={installedApp} />
)}
{installedApp.app.mode === 'workflow' && (
<TextGenerationApp isWorkflow isInstalledApp installedAppInfo={installedApp}/>
<TextGenerationApp isWorkflow isInstalledApp installedAppInfo={installedApp} />
)}
</div>
)

View File

@ -11,9 +11,11 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import TabHeader from '../../base/tab-header'
import Button from '../../base/button'
import { checkOrSetAccessToken } from '../utils'
import AppUnavailable from '../../base/app-unavailable'
import s from './style.module.css'
import RunBatch from './run-batch'
import ResDownload from './run-batch/res-download'
import MenuDropdown from './menu-dropdown'
import cn from '@/utils/classnames'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
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 { Resolution, TransferMethod } from '@/types/app'
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.
enum TaskStatus {
@ -106,6 +110,9 @@ const TextGeneration: FC<IMainProps> = ({
const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | 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
const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([])
const fetchSavedMessage = async () => {
@ -537,12 +544,14 @@ const TextGeneration: FC<IMainProps> = ({
</div>
)
if (!appId || !siteInfo || !promptConfig) {
if (!appId || !siteInfo || !promptConfig || isGettingAccessMode || isCheckingPermission) {
return (
<div className='flex items-center h-screen'>
<Loading type='app' />
</div>)
}
if (!userCanAccessResult?.result)
return <AppUnavailable code={403} unknownReason='no permission.' />
return (
<>
@ -558,16 +567,19 @@ const TextGeneration: FC<IMainProps> = ({
'shrink-0 relative flex flex-col pb-10 h-full border-r border-gray-100 bg-white',
)}>
<div className='mb-6'>
<div className='flex items-center justify-between'>
<div className='flex items-center space-x-3'>
<AppIcon
size="small"
iconType={siteInfo.icon_type}
icon={siteInfo.icon}
background={siteInfo.icon_background || appDefaultIconBackground}
imageUrl={siteInfo.icon_url}
/>
<div className='text-lg font-semibold text-gray-800'>{siteInfo.title}</div>
<div className='flex items-center'>
<div className='flex grow'>
<div className='flex items-center space-x-3 grow'>
<AppIcon
size="small"
iconType={siteInfo.icon_type}
icon={siteInfo.icon}
background={siteInfo.icon_background || appDefaultIconBackground}
imageUrl={siteInfo.icon_url}
/>
<div className='text-lg font-semibold text-gray-800'>{siteInfo.title}</div>
</div>
<MenuDropdown hideLogout={isInstalledApp || appAccessMode?.accessMode === AccessMode.PUBLIC} />
</div>
{!isPC && (
<Button

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

View File

@ -30,26 +30,26 @@ const translation = {
overview: {
title: 'Übersicht',
appInfo: {
explanation: 'Einsatzbereite AI-WebApp',
explanation: 'Einsatzbereite AI-web app',
accessibleAddress: 'Öffentliche URL',
preview: 'Vorschau',
regenerate: 'Regenerieren',
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: {
entry: 'Einstellungen',
title: 'WebApp-Einstellungen',
webName: 'WebApp-Name',
webDesc: 'WebApp-Beschreibung',
title: 'web app Einstellungen',
webName: 'web app Name',
webDesc: 'web app Beschreibung',
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',
workflow: {
title: 'Workflow-Schritte',
show: 'Anzeigen',
hide: 'Verbergen',
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',
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',
},
sso: {
title: 'WebApp-SSO',
description: 'Alle Benutzer müssen sich mit SSO anmelden, bevor sie WebApp verwenden können',
title: 'web app SSO',
description: 'Alle Benutzer müssen sich mit SSO anmelden, bevor sie web app verwenden können',
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.',
},
@ -94,7 +94,7 @@ const translation = {
customize: {
way: 'Art',
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.',
way1: {
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: {
descriptionInExplore: 'Gibt an, ob das WebApp-Symbol zum Ersetzen 🤖 in Explore verwendet werden soll',
title: 'Verwenden Sie das WebApp-Symbol, um es zu ersetzen 🤖',
description: 'Gibt an, ob das WebApp-Symbol zum Ersetzen 🤖 in der freigegebenen Anwendung verwendet werden soll',
descriptionInExplore: 'Gibt an, ob das web app Symbol zum Ersetzen 🤖 in Explore verwendet werden soll',
title: 'Verwenden Sie das web app Symbol, um es zu ersetzen 🤖',
description: 'Gibt an, ob das web app Symbol zum Ersetzen 🤖 in der freigegebenen Anwendung verwendet werden soll',
},
importFromDSLUrlPlaceholder: 'DSL-Link hier einfügen',
duplicate: 'Duplikat',

View File

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

View File

@ -30,27 +30,27 @@ const translation = {
overview: {
title: 'Overview',
appInfo: {
explanation: 'Ready-to-use AI WebApp',
explanation: 'Ready-to-use AI web app',
accessibleAddress: 'Public URL',
preview: 'Preview',
regenerate: 'Regenerate',
regenerateNotice: 'Do you want to regenerate the public URL?',
preUseReminder: 'Please enable WebApp before continuing.',
preUseReminder: 'Please enable web app before continuing.',
settings: {
entry: 'Settings',
title: 'Web App Settings',
modalTip: 'Client-side web app settings. ',
webName: 'WebApp Name',
webDesc: 'WebApp Description',
webName: 'web app Name',
webDesc: 'web app Description',
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',
workflow: {
title: 'Workflow',
subTitle: 'Workflow Details',
show: 'Show',
hide: 'Hide',
showDesc: 'Show or hide workflow details in WebApp',
showDesc: 'Show or hide workflow details in web app',
},
chatColorTheme: 'Chat color theme',
chatColorThemeDesc: 'Set the color theme of the chatbot',
@ -58,14 +58,14 @@ const translation = {
invalidHexMessage: 'Invalid hex value',
sso: {
label: 'SSO Enforcement',
title: 'WebApp SSO',
description: 'All users are required to login with SSO before using WebApp',
tooltip: 'Contact the administrator to enable WebApp SSO',
title: 'web app SSO',
description: 'All users are required to login with SSO before using web app',
tooltip: 'Contact the administrator to enable web app SSO',
},
more: {
entry: 'Show more settings',
copyright: 'Copyright',
copyrightTip: 'Display copyright information in the webapp',
copyrightTip: 'Display copyright information in the web app',
copyrightTooltip: 'Please upgrade to Professional plan or above',
copyRightPlaceholder: 'Enter the name of the author or organization',
privacyPolicy: 'Privacy Policy',
@ -94,7 +94,7 @@ const translation = {
customize: {
way: 'way',
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.',
way1: {
name: 'Fork the client code, modify it and deploy to Vercel (recommended)',

View File

@ -112,9 +112,9 @@ const translation = {
image: 'Image',
},
answerIcon: {
title: 'Use WebApp icon to replace 🤖',
description: 'Whether to use the WebApp icon to replace 🤖 in the shared application',
descriptionInExplore: 'Whether to use the WebApp icon to replace 🤖 in Explore',
title: 'Use web app icon to replace 🤖',
description: 'Whether to use the web app icon to replace 🤖 in the shared application',
descriptionInExplore: 'Whether to use the web app icon to replace 🤖 in Explore',
},
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 ',
@ -174,6 +174,36 @@ const translation = {
},
},
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

View File

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

View File

@ -5,7 +5,7 @@ const translation = {
suffix: 'customize your brand.',
},
webapp: {
title: 'Customize WebApp brand',
title: 'Customize web app brand',
removeBrand: 'Remove Powered by Dify',
changeLogo: 'Change Powered by Brand Image',
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.',
licenseInactive: 'License Inactive',
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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,12 +30,12 @@ const translation = {
overview: {
title: 'Aperçu',
appInfo: {
explanation: 'WebApp AI prête à l\'emploi',
explanation: 'web app AI prête à l\'emploi',
accessibleAddress: 'URL publique',
preview: 'Aperçu',
regenerate: 'Regénérer',
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: {
entry: 'Paramètres',
title: 'Paramètres de l\'application Web',
@ -48,7 +48,7 @@ const translation = {
title: 'Étapes du workflow',
show: 'Afficher',
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',
},
chatColorTheme: 'Thème de couleur du chatbot',
@ -70,9 +70,9 @@ const translation = {
},
sso: {
label: 'Authentification SSO',
title: 'WebApp SSO',
tooltip: 'Contactez ladministrateur pour activer lauthentification unique WebApp',
description: 'Tous les utilisateurs doivent se connecter avec lauthentification unique avant dutiliser WebApp',
title: 'web app SSO',
tooltip: 'Contactez ladministrateur pour activer lauthentification unique web app',
description: 'Tous les utilisateurs doivent se connecter avec lauthentification unique avant dutiliser web app',
},
modalTip: 'Paramètres de lapplication web côté client.',
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,27 +33,27 @@ const translation = {
overview: {
title: 'Panoramica',
appInfo: {
explanation: 'AI WebApp pronta all\'uso',
explanation: 'AI web app pronta all\'uso',
accessibleAddress: 'URL Pubblico',
preview: 'Anteprima',
regenerate: 'Rigenera',
regenerateNotice: 'Vuoi rigenerare l\'URL pubblico?',
preUseReminder: 'Attiva WebApp prima di continuare.',
preUseReminder: 'Attiva web app prima di continuare.',
settings: {
entry: 'Impostazioni',
title: 'Impostazioni WebApp',
webName: 'Nome WebApp',
webDesc: 'Descrizione WebApp',
title: 'Impostazioni web app',
webName: 'Nome web app',
webDesc: 'Descrizione web app',
webDescTip:
'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',
workflow: {
title: 'Fasi del Workflow',
show: 'Mostra',
hide: 'Nascondi',
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',
chatColorThemeDesc: 'Imposta il tema colore del chatbot',
@ -73,14 +73,14 @@ const translation = {
'Inserisci il testo del disclaimer personalizzato',
customDisclaimerTip:
'Il testo del disclaimer personalizzato verrà visualizzato sul lato client, fornendo informazioni aggiuntive sull\'applicazione',
copyrightTip: 'Visualizzare le informazioni sul copyright nella webapp',
copyrightTip: 'Visualizzare le informazioni sul copyright nella web app',
copyrightTooltip: 'Si prega di eseguire l\'upgrade al piano Professional o superiore',
},
sso: {
label: 'Autenticazione SSO',
title: 'WebApp SSO',
description: 'Tutti gli utenti devono effettuare l\'accesso con SSO prima di utilizzare WebApp',
tooltip: 'Contattare l\'amministratore per abilitare l\'SSO di WebApp',
title: 'web app SSO',
description: 'Tutti gli utenti devono effettuare l\'accesso con SSO prima di utilizzare web app',
tooltip: 'Contattare l\'amministratore per abilitare l\'SSO di web app',
},
modalTip: 'Impostazioni dell\'app Web lato client.',
},
@ -104,7 +104,7 @@ const translation = {
customize: {
way: 'modo',
entry: 'Personalizza',
title: 'Personalizza AI WebApp',
title: 'Personalizza AI web app',
explanation:
'Puoi personalizzare il frontend della Web App per adattarla alle tue esigenze di scenario e stile.',
way1: {

View File

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

View File

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

View File

@ -189,6 +189,36 @@ const translation = {
searchAllTemplate: 'すべてのテンプレートを検索...',
},
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,15 +38,15 @@ const translation = {
preview: 'Podgląd',
regenerate: 'Wygeneruj ponownie',
regenerateNotice: 'Czy chcesz wygenerować ponownie publiczny adres URL?',
preUseReminder: 'Przed kontynuowaniem włącz aplikację WebApp.',
preUseReminder: 'Przed kontynuowaniem włącz aplikację web app.',
settings: {
entry: 'Ustawienia',
title: 'Ustawienia WebApp',
webName: 'Nazwa WebApp',
webDesc: 'Opis WebApp',
title: 'Ustawienia web app',
webName: 'Nazwa web app',
webDesc: 'Opis web app',
webDescTip:
'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',
workflow: {
title: 'Kroki przepływu pracy',

View File

@ -169,7 +169,7 @@ const translation = {
},
answerIcon: {
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',
},
importFromDSL: 'Importowanie z DSL',

View File

@ -30,26 +30,26 @@ const translation = {
overview: {
title: 'Visão Geral',
appInfo: {
explanation: 'WebApp de IA Pronta para Uso',
explanation: 'web app de IA Pronta para Uso',
accessibleAddress: 'URL Pública',
preview: 'Visualização',
regenerate: 'Regenerar',
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: {
entry: 'Configurações',
title: 'Configurações do WebApp',
webName: 'Nome do WebApp',
webDesc: 'Descrição do WebApp',
title: 'Configurações do web app',
webName: 'Nome do web app',
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',
webDescPlaceholder: 'Insira a descrição do WebApp',
webDescPlaceholder: 'Insira a descrição do web app',
language: 'Idioma',
workflow: {
title: 'Etapas do fluxo de trabalho',
show: 'Mostrar',
hide: 'Ocultar',
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',
chatColorThemeDesc: 'Defina o tema de cor do chatbot',
@ -65,14 +65,14 @@ const translation = {
customDisclaimer: 'Aviso Legal Personalizado',
customDisclaimerPlaceholder: 'Insira o texto do aviso legal',
customDisclaimerTip: 'O texto do aviso legal personalizado será exibido no lado do cliente, fornecendo informações adicionais sobre o aplicativo',
copyrightTip: 'Exibir informações de direitos autorais no webapp',
copyrightTip: 'Exibir informações de direitos autorais no web app',
copyrightTooltip: 'Por favor, atualize para o plano Professional ou superior',
},
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',
title: 'WebApp SSO',
description: 'Todos os usuários devem fazer login com SSO antes de usar o WebApp',
title: 'web app SSO',
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.',
},
@ -94,7 +94,7 @@ const translation = {
customize: {
way: 'modo',
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.',
way1: {
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: {
descriptionInExplore: 'Se o ícone do WebApp deve ser usado para substituir 🤖 no Explore',
description: 'Se o ícone WebApp deve ser usado para substituir 🤖 no aplicativo compartilhado',
title: 'Use o ícone do WebApp para substituir 🤖',
descriptionInExplore: 'Se o ícone do web app deve ser usado para substituir 🤖 no Explore',
description: 'Se o ícone web app deve ser usado para substituir 🤖 no aplicativo compartilhado',
title: 'Use o ícone do web app para substituir 🤖',
},
importFromDSLUrlPlaceholder: 'Cole o link DSL aqui',
importFromDSLUrl: 'Do URL',

View File

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

View File

@ -49,7 +49,7 @@ const translation = {
show: 'Afișați',
hide: 'Ascundeți',
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',
chatColorThemeDesc: 'Setați tema de culoare a chatbotului',
@ -70,9 +70,9 @@ const translation = {
},
sso: {
label: 'Autentificare SSO',
title: 'WebApp SSO',
description: 'Toți utilizatorii trebuie să se conecteze cu SSO înainte de a utiliza WebApp',
tooltip: 'Contactați administratorul pentru a activa WebApp SSO',
title: 'web app SSO',
description: 'Toți utilizatorii trebuie să se conecteze cu SSO înainte de a utiliza web app',
tooltip: 'Contactați administratorul pentru a activa web app SSO',
},
modalTip: 'Setările aplicației web pe partea clientului.',
},

View File

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

View File

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

View File

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

View File

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

View File

@ -109,9 +109,9 @@ const translation = {
image: 'Slika',
},
answerIcon: {
title: 'Uporabite ikono WebApp za zamenjavo 🤖',
description: 'Ali uporabiti ikono WebApp za zamenjavo 🤖 v deljeni aplikaciji',
descriptionInExplore: 'Ali uporabiti ikono WebApp za zamenjavo 🤖 v razdelku Razišči',
title: 'Uporabite ikono web app za zamenjavo 🤖',
description: 'Ali uporabiti ikono web app za zamenjavo 🤖 v deljeni aplikaciji',
descriptionInExplore: 'Ali uporabiti ikono web app za zamenjavo 🤖 v razdelku Razišči',
},
switch: 'Preklopi na Workflow Orchestrate',
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: {
title: 'ภาพรวม',
appInfo: {
explanation: 'AI WebApp พร้อมใช้งาน',
explanation: 'AI web app พร้อมใช้งาน',
accessibleAddress: 'URL สาธารณะ',
preview: 'ดูตัวอย่าง',
regenerate: 'สร้างใหม่',
regenerateNotice: 'คุณต้องการสร้าง URL สาธารณะใหม่หรือไม่',
preUseReminder: 'โปรดเปิดใช้งาน WebApp ก่อนดําเนินการต่อ',
preUseReminder: 'โปรดเปิดใช้งาน web app ก่อนดําเนินการต่อ',
settings: {
entry: 'การตั้งค่า',
title: 'การตั้งค่าเว็บแอป',
webName: 'ชื่อเว็บแอป',
webDesc: 'คําอธิบาย WebApp',
webDesc: 'คําอธิบาย web app',
webDescTip: 'ข้อความนี้จะแสดงที่ฝั่งไคลเอ็นต์ โดยให้คําแนะนําพื้นฐานเกี่ยวกับวิธีการใช้แอปพลิเคชัน',
webDescPlaceholder: 'ป้อนคําอธิบายของ WebApp',
webDescPlaceholder: 'ป้อนคําอธิบายของ web app',
language: 'ภาษา',
workflow: {
title: 'เวิร์กโฟลว์',
subTitle: 'รายละเอียดเวิร์กโฟลว์',
show: 'แสดง',
hide: 'ซ่อน',
showDesc: 'แสดงหรือซ่อนรายละเอียดเวิร์กโฟลว์ใน WebApp',
showDesc: 'แสดงหรือซ่อนรายละเอียดเวิร์กโฟลว์ใน web app',
},
chatColorTheme: 'ธีมสีแชท',
chatColorThemeDesc: 'กําหนดธีมสีของแชทบอท',
@ -58,8 +58,8 @@ const translation = {
sso: {
label: 'การรับรองความถูกต้องของ SSO',
title: 'เว็บแอป SSO',
description: 'ผู้ใช้ทุกคนต้องเข้าสู่ระบบด้วย SSO ก่อนใช้ WebApp',
tooltip: 'ติดต่อผู้ดูแลระบบเพื่อเปิดใช้ WebApp SSO',
description: 'ผู้ใช้ทุกคนต้องเข้าสู่ระบบด้วย SSO ก่อนใช้ web app',
tooltip: 'ติดต่อผู้ดูแลระบบเพื่อเปิดใช้ web app SSO',
},
more: {
entry: 'แสดงการตั้งค่าเพิ่มเติม',
@ -94,7 +94,7 @@ const translation = {
customize: {
way: 'วิธี',
entry: 'ปรับแต่ง',
title: 'ปรับแต่ง AI WebApp',
title: 'ปรับแต่ง AI web app',
explanation: 'คุณสามารถปรับแต่งส่วนหน้าของ Web App ให้เหมาะกับสถานการณ์และความต้องการสไตล์ของคุณได้',
way1: {
name: 'แยกรหัสไคลเอ็นต์ แก้ไข และปรับใช้กับ Vercel (แนะนํา)',

View File

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

View File

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

View File

@ -30,25 +30,25 @@ const translation = {
overview: {
title: 'Genel Bakış',
appInfo: {
explanation: 'Kullanıma hazır AI WebApp',
explanation: 'Kullanıma hazır AI web app',
accessibleAddress: 'Genel URL',
preview: 'Önizleme',
regenerate: 'Yeniden Oluştur',
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: {
entry: 'Ayarlar',
title: 'WebApp Ayarları',
webName: 'WebApp İsmi',
webDesc: 'WebApp Açıklaması',
title: 'web app Ayarları',
webName: 'web app İsmi',
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',
webDescPlaceholder: 'WebApp\'in açıklamasını girin',
webDescPlaceholder: 'web app\'in açıklamasını girin',
language: 'Dil',
workflow: {
title: 'Workflow Adımları',
show: 'Göster',
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ı',
},
chatColorTheme: 'Sohbet renk teması',
@ -69,10 +69,10 @@ const translation = {
copyrightTooltip: 'Lütfen Profesyonel plana veya daha yüksek bir plana yükseltin',
},
sso: {
title: 'WebApp SSO\'su',
tooltip: 'WebApp SSO\'yu etkinleştirmek için yöneticiyle iletişime geçin',
title: 'web app SSO\'su',
tooltip: 'web app SSO\'yu etkinleştirmek için yöneticiyle iletişime geçin',
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ı.',
},
@ -94,7 +94,7 @@ const translation = {
customize: {
way: 'yol',
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.',
way1: {
name: 'İstemci kodunu forklayarak değiştirin ve Vercel\'e dağıtın (önerilen)',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -48,7 +48,7 @@ const translation = {
title: 'Các bước quy trình',
show: 'Hiển thị',
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',
},
chatColorTheme: 'Giao diện màu trò chuyện',
@ -70,8 +70,8 @@ const translation = {
},
sso: {
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',
tooltip: 'Liên hệ với quản trị viên để bật SSO 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 web app',
label: 'Xác thực SSO',
},
modalTip: 'Cài đặt ứng dụng web phía máy khách.',

View File

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

View File

@ -5,7 +5,7 @@ const translation = {
suffix: 'tùy chỉnh thương hiệu.',
},
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"',
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',

View File

@ -214,7 +214,7 @@ const translation = {
modalTitle: '图片上传设置',
},
bar: {
empty: '开启功能增强 webapp 用户体验',
empty: '开启功能增强 web app 用户体验',
enableText: '功能已开启',
manage: '管理',
},

View File

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

View File

@ -113,9 +113,9 @@ const translation = {
image: '图片',
},
answerIcon: {
title: '使用 WebApp 图标替换 🤖',
description: '是否使用 WebApp 图标替换分享的应用界面中的 🤖',
descriptionInExplore: '是否使用 WebApp 图标替换 Explore 界面中的 🤖',
title: '使用 web app 图标替换 🤖',
description: '是否使用 web app 图标替换分享的应用界面中的 🤖',
descriptionInExplore: '是否使用 web app 图标替换 Explore 界面中的 🤖',
},
switch: '迁移为工作流编排',
switchTipStart: '将为您创建一个使用工作流编排的新应用。新应用将',
@ -175,6 +175,36 @@ const translation = {
},
openInExplore: '在“探索”中打开',
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ const translation = {
suffix: '定製您的品牌。',
},
webapp: {
title: '定製 WebApp 品牌',
title: '定製 web app 品牌',
removeBrand: '移除 Powered by Dify',
changeLogo: '更改 Powered by Brand 圖片',
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 { App, AppSSO, AppTemplate, SiteConfig } from '@/types/app'
import type { App, AppTemplate, SiteConfig } from '@/types/app'
/* export type App = {
id: string
@ -89,8 +89,6 @@ export type DSLImportResponse = {
error: string
}
export type AppSSOResponse = { enabled: AppSSO['enable_sso'] }
export type AppTemplatesResponse = {
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 { 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 { AppIconType, AppMode, ModelConfig } from '@/types/app'
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}`)
}
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 }) => {
return get<AppTemplatesResponse>(url)
}

View File

@ -512,6 +512,15 @@ export const ssePost = (
}).catch(() => {
res.json().then((data: any) => {
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')
requiredWebSSOLogin()
@ -566,6 +575,16 @@ export const request = async<T>(url: string, options = {}, otherOptions?: IOther
// special code
const { code, message } = errRespData
// 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') {
requiredWebSSOLogin()
return Promise.reject(err)

View File

@ -11,7 +11,7 @@ import type {
ConversationItem,
} from '@/models/share'
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) {
switch (action) {
@ -144,10 +144,6 @@ export const fetchAppParams = async (isInstalledApp: boolean, installedAppId = '
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) => {
return (getAction('get', false))(getUrl('/enterprise/sso/saml/login', false, ''), {
params: {
@ -225,3 +221,17 @@ export const fetchAccessToken = async (appCode: string) => {
headers.append('X-App-Code', appCode)
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-orange-gradient': 'var(--color-dataset-option-card-orange-gradient)',
'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: {
'20': '20',

View File

@ -1,35 +1,26 @@
html[data-theme="dark"] {
--color-chatbot-bg: linear-gradient(
180deg,
rgba(34, 34, 37, 0.9) 0%,
rgba(29, 29, 32, 0.9) 90.48%
);
--color-chat-bubble-bg: linear-gradient(
180deg,
rgba(200, 206, 218, 0.08) 0%,
rgba(200, 206, 218, 0.02) 100%
);
--color-workflow-process-bg: linear-gradient(
90deg,
rgba(24, 24, 27, 0.25) 0%,
rgba(24, 24, 27, 0.04) 100%
);
--color-account-teams-bg: linear-gradient(
271deg,
rgba(34, 34, 37, 0.9) -0.1%,
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-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-child-chunk-expand-btn-bg: linear-gradient(90deg, rgba(24, 24, 27, 0.25) 0%, rgba(24, 24, 27, 0.04) 100%);
--color-dataset-option-card-blue-gradient: linear-gradient(90deg, #24252E 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-chunk-list-mask-bg: linear-gradient(180deg, rgba(34, 34, 37, 0.00) 0%, #222225 100%);
--mask-top2bottom-gray-50-to-transparent: linear-gradient(
180deg,
rgba(24, 24, 27, 0.08) 0%,
rgba(0, 0, 0, 0) 100%
);
}
--color-chatbot-bg: linear-gradient(180deg,
rgba(34, 34, 37, 0.9) 0%,
rgba(29, 29, 32, 0.9) 90.48%);
--color-chat-bubble-bg: linear-gradient(180deg,
rgba(200, 206, 218, 0.08) 0%,
rgba(200, 206, 218, 0.02) 100%);
--color-workflow-process-bg: linear-gradient(90deg,
rgba(24, 24, 27, 0.25) 0%,
rgba(24, 24, 27, 0.04) 100%);
--color-account-teams-bg: linear-gradient(271deg,
rgba(34, 34, 37, 0.9) -0.1%,
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-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-child-chunk-expand-btn-bg: linear-gradient(90deg, rgba(24, 24, 27, 0.25) 0%, rgba(24, 24, 27, 0.04) 100%);
--color-dataset-option-card-blue-gradient: linear-gradient(90deg, #24252E 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-chunk-list-mask-bg: linear-gradient(180deg, rgba(34, 34, 37, 0.00) 0%, #222225 100%);
--mask-top2bottom-gray-50-to-transparent: linear-gradient(180deg,
rgba(24, 24, 27, 0.08) 0%,
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,35 +1,26 @@
html[data-theme="light"] {
--color-chatbot-bg: linear-gradient(
180deg,
rgba(249, 250, 251, 0.9) 0%,
rgba(242, 244, 247, 0.9) 90.48%
);
--color-chat-bubble-bg: linear-gradient(
180deg,
#fff 0%,
rgba(255, 255, 255, 0.6) 100%
);
--color-workflow-process-bg: linear-gradient(
90deg,
rgba(200, 206, 218, 0.2) 0%,
rgba(200, 206, 218, 0.04) 100%
);
--color-account-teams-bg: linear-gradient(
271deg,
rgba(249, 250, 251, 0.9) -0.1%,
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-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-child-chunk-expand-btn-bg: linear-gradient(90deg, rgba(200, 206, 218, 0.20) 0%, rgba(200, 206, 218, 0.04) 100%);
--color-dataset-option-card-blue-gradient: linear-gradient(90deg, #F2F4F7 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-chunk-list-mask-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FCFCFD 100%);
--mask-top2bottom-gray-50-to-transparent: linear-gradient(
180deg,
rgba(200, 206, 218, 0.2) 0%,
rgba(255, 255, 255, 0) 100%
);
}
--color-chatbot-bg: linear-gradient(180deg,
rgba(249, 250, 251, 0.9) 0%,
rgba(242, 244, 247, 0.9) 90.48%);
--color-chat-bubble-bg: linear-gradient(180deg,
#fff 0%,
rgba(255, 255, 255, 0.6) 100%);
--color-workflow-process-bg: linear-gradient(90deg,
rgba(200, 206, 218, 0.2) 0%,
rgba(200, 206, 218, 0.04) 100%);
--color-account-teams-bg: linear-gradient(271deg,
rgba(249, 250, 251, 0.9) -0.1%,
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-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-child-chunk-expand-btn-bg: linear-gradient(90deg, rgba(200, 206, 218, 0.20) 0%, rgba(200, 206, 218, 0.04) 100%);
--color-dataset-option-card-blue-gradient: linear-gradient(90deg, #F2F4F7 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-chunk-list-mask-bg: linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FCFCFD 100%);
--mask-top2bottom-gray-50-to-transparent: linear-gradient(180deg,
rgba(200, 206, 218, 0.2) 0%,
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%);
}

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