Signed-off-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: Hash Brown <hi@xzd.me>
Co-authored-by: crazywoola <427733928@qq.com>
Co-authored-by: GareArc <chen4851@purdue.edu>
Co-authored-by: Byron.wang <byron@dify.ai>
Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: Garfield Dai <dai.hai@foxmail.com>
Co-authored-by: KVOJJJin <jzongcode@gmail.com>
Co-authored-by: Alexi.F <654973939@qq.com>
Co-authored-by: Xiyuan Chen <52963600+GareArc@users.noreply.github.com>
Co-authored-by: kautsar_masuara <61046989+izon-masuara@users.noreply.github.com>
Co-authored-by: achmad-kautsar <achmad.kautsar@insignia.co.id>
Co-authored-by: Xin Zhang <sjhpzx@gmail.com>
Co-authored-by: kelvintsim <83445753+kelvintsim@users.noreply.github.com>
Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: Zixuan Cheng <61724187+Theysua@users.noreply.github.com>
This commit is contained in:
NFish
2025-05-20 12:07:50 +08:00
committed by GitHub
parent 6a8ca8296b
commit d186daa131
199 changed files with 3618 additions and 1000 deletions

View File

@ -15,17 +15,17 @@ import {
} from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
import { useContextSelector } from 'use-context-selector'
import s from './style.module.css'
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 AppContext, { useAppContext } from '@/context/app-context'
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'
import type { App } from '@/types/app'
import useDocumentTitle from '@/hooks/use-document-title'
export type IAppDetailLayoutProps = {
children: React.ReactNode
@ -56,7 +56,6 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
icon: NavIcon
selectedIcon: NavIcon
}>>([])
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
const navs = [
@ -96,9 +95,10 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
return navs
}, [])
useDocumentTitle(appDetail?.name || t('common.menus.appDetail'))
useEffect(() => {
if (appDetail) {
document.title = `${(appDetail.name || 'App')} - Dify`
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
const mode = isMobile ? 'collapse' : 'expand'
setAppSiderbarExpand(isMobile ? mode : localeMode)
@ -142,14 +142,9 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
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 })
})
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appDetailRes, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace, systemFeatures.enable_web_sso_switch_component])
}, [appDetailRes, isCurrentWorkspaceEditor, isLoadingAppDetail, isLoadingCurrentWorkspace])
useUnmount(() => {
setAppDetail()

View File

@ -2,25 +2,22 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { useContext, useContextSelector } from 'use-context-selector'
import { useContext } from 'use-context-selector'
import AppCard from '@/app/components/app/overview/appCard'
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 AppContext from '@/context/app-context'
export type ICardViewProps = {
appId: string
@ -33,18 +30,11 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
const { notify } = useContext(ToastContext)
const appDetail = useAppStore(state => state.appDetail)
const setAppDetail = useAppStore(state => state.setAppDetail)
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
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) }
}
@ -95,16 +85,6 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
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

@ -2,7 +2,9 @@
import type { FC } from 'react'
import React, { useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context'
import useDocumentTitle from '@/hooks/use-document-title'
export type IAppDetail = {
children: React.ReactNode
@ -11,12 +13,13 @@ export type IAppDetail = {
const AppDetail: FC<IAppDetail> = ({ children }) => {
const router = useRouter()
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
const { t } = useTranslation()
useDocumentTitle(t('common.menus.appDetail'))
useEffect(() => {
if (isCurrentWorkspaceDatasetOperator)
return router.replace('/datasets')
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isCurrentWorkspaceDatasetOperator])
}, [isCurrentWorkspaceDatasetOperator, router])
return (
<>

View File

@ -4,7 +4,8 @@ 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 cn from '@/utils/classnames'
import type { App } from '@/types/app'
import Confirm from '@/app/components/base/confirm'
import Toast, { ToastContext } from '@/app/components/base/toast'
@ -30,7 +31,10 @@ 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 cn from '@/utils/classnames'
import Tooltip from '@/app/components/base/tooltip'
import AccessControl from '@/app/components/app/app-access-control'
import { AccessMode } from '@/models/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type AppCardProps = {
app: App
@ -40,6 +44,7 @@ export type AppCardProps = {
const AppCard = ({ app, onRefresh }: AppCardProps) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const { isCurrentWorkspaceEditor } = useAppContext()
const { onPlanInfoChanged } = useProviderContext()
const { push } = useRouter()
@ -53,6 +58,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,8 +77,7 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
})
}
setShowConfirmDelete(false)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [app.id])
}, [app.id, mutateApps, notify, onPlanInfoChanged, onRefresh, t])
const onEdit: CreateAppModalProps['onConfirm'] = useCallback(async ({
name,
@ -176,6 +181,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?.()
@ -198,18 +210,24 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
e.preventDefault()
exportCheck()
}
const onClickSwitch = async (e: React.MouseEvent<HTMLDivElement>) => {
const onClickSwitch = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
props.onClick?.()
e.preventDefault()
setShowSwitchModal(true)
}
const onClickDelete = async (e: React.MouseEvent<HTMLDivElement>) => {
const onClickDelete = async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
props.onClick?.()
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?.()
@ -226,41 +244,49 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
}
}
return (
<div className="relative w-full py-1" onMouseLeave={onMouseLeave}>
<button className='mx-1 flex h-8 w-[calc(100%_-_8px)] cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover' onClick={onClickSettings}>
<div className="relative flex w-full flex-col py-1" onMouseLeave={onMouseLeave}>
<button className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickSettings}>
<span className='system-sm-regular text-text-secondary'>{t('app.editApp')}</span>
</button>
<Divider className="!my-1" />
<button className='mx-1 flex h-8 w-[calc(100%_-_8px)] cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover' onClick={onClickDuplicate}>
<Divider className="my-1" />
<button className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickDuplicate}>
<span className='system-sm-regular text-text-secondary'>{t('app.duplicate')}</span>
</button>
<button className='mx-1 flex h-8 w-[calc(100%_-_8px)] cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover' onClick={onClickExport}>
<button className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickExport}>
<span className='system-sm-regular text-text-secondary'>{t('app.export')}</span>
</button>
{(app.mode === 'completion' || app.mode === 'chat') && (
<>
<Divider className="!my-1" />
<div
className='mx-1 flex h-9 cursor-pointer items-center rounded-lg px-3 py-2 hover:bg-state-base-hover'
<Divider className="my-1" />
<button
className='mx-1 flex h-8 cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover'
onClick={onClickSwitch}
>
<span className='text-sm leading-5 text-text-secondary'>{t('app.switch')}</span>
</div>
</button>
</>
)}
<Divider className="!my-1" />
<button className='mx-1 flex h-8 w-[calc(100%_-_8px)] cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-base-hover' onClick={onClickInstalledApp}>
<Divider className="my-1" />
<button className='mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickInstalledApp}>
<span className='system-sm-regular text-text-secondary'>{t('app.openInExplore')}</span>
</button>
<Divider className="!my-1" />
<div
className='group mx-1 flex h-8 w-[calc(100%_-_8px)] cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-destructive-hover'
<Divider className="my-1" />
{
systemFeatures.webapp_auth.enabled && isCurrentWorkspaceEditor && <>
<button className='mx-1 flex h-8 cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover' onClick={onClickAccessControl}>
<span className='text-sm leading-5 text-text-secondary'>{t('app.accessControl')}</span>
</button>
<Divider className='my-1' />
</>
}
<button
className='group mx-1 flex h-8 cursor-pointer items-center gap-2 rounded-lg px-3 py-[6px] hover:bg-state-destructive-hover'
onClick={onClickDelete}
>
<span className='system-sm-regular text-text-secondary group-hover:text-text-destructive'>
{t('common.operation.delete')}
</span>
</div>
</button>
</div>
)
}
@ -302,6 +328,17 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
{app.mode === 'completion' && <div className='truncate'>{t('app.types.completion').toUpperCase()}</div>}
</div>
</div>
<div className='flex h-5 w-5 shrink-0 items-center justify-center'>
{app.access_mode === AccessMode.PUBLIC && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.anyone')}>
<RiGlobalLine className='h-4 w-4 text-text-accent' />
</Tooltip>}
{app.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.specific')}>
<RiLockLine className='h-4 w-4 text-text-quaternary' />
</Tooltip>}
{app.access_mode === AccessMode.ORGANIZATION && <Tooltip asChild={false} popupContent={t('app.accessItemsDescription.organization')}>
<RiBuildingLine className='h-4 w-4 text-text-quaternary' />
</Tooltip>}
</div>
</div>
<div className='title-wrapper h-[90px] px-[14px] text-xs leading-normal text-text-tertiary'>
<div
@ -358,7 +395,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={'!z-20 h-fit'}
/>
@ -419,6 +456,9 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => {
onClose={() => setSecretEnvList([])}
/>
)}
{showAccessControl && (
<AccessControl app={app} onConfirm={onUpdateAccessControl} onClose={() => setShowAccessControl(false)} />
)}
</>
)
}

View File

@ -96,7 +96,6 @@ const Apps = () => {
]
useEffect(() => {
document.title = `${t('common.menus.apps')} - Dify`
if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
mutate()

View File

@ -0,0 +1,12 @@
'use client'
import useDocumentTitle from '@/hooks/use-document-title'
import { useTranslation } from 'react-i18next'
export default function DatasetsLayout({ children }: { children: React.ReactNode }) {
const { t } = useTranslation()
useDocumentTitle(t('common.menus.apps'))
return (<>
{children}
</>)
}

View File

@ -1,24 +1,20 @@
'use client'
import { useContextSelector } from 'use-context-selector'
import { useTranslation } from 'react-i18next'
import { RiDiscordFill, RiGithubFill } from '@remixicon/react'
import Link from 'next/link'
import style from '../list.module.css'
import Apps from './Apps'
import AppContext from '@/context/app-context'
import { LicenseStatus } from '@/types/feature'
import { useEducationInit } from '@/app/education-apply/hooks'
import { useGlobalPublicStore } from '@/context/global-public-context'
const AppList = () => {
const { t } = useTranslation()
useEducationInit()
const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures)
const { systemFeatures } = useGlobalPublicStore()
return (
<div className='relative flex h-0 shrink-0 grow flex-col overflow-y-auto bg-background-body'>
<Apps />
{systemFeatures.license.status === LicenseStatus.NONE && <footer className='shrink-0 grow-0 px-12 py-6'>
{!systemFeatures.branding.enabled && <footer className='shrink-0 grow-0 px-12 py-6'>
<h3 className='text-gradient text-xl font-semibold leading-tight'>{t('app.join')}</h3>
<p className='system-sm-regular mt-1 text-text-tertiary'>{t('app.communityIntro')}</p>
<div className='mt-3 flex items-center gap-2'>

View File

@ -31,6 +31,7 @@ import { getLocaleOnClient } from '@/i18n'
import { useAppContext } from '@/context/app-context'
import Tooltip from '@/app/components/base/tooltip'
import LinkedAppsPanel from '@/app/components/base/linked-apps-panel'
import useDocumentTitle from '@/hooks/use-document-title'
export type IAppDetailLayoutProps = {
children: React.ReactNode
@ -158,10 +159,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
return baseNavigation
}, [datasetRes?.provider, datasetId, t])
useEffect(() => {
if (datasetRes)
document.title = `${datasetRes.name || 'Dataset'} - Dify`
}, [datasetRes])
useDocumentTitle(datasetRes?.name || t('common.menus.datasets'))
const setAppSiderbarExpand = useStore(state => state.setAppSiderbarExpand)

View File

@ -29,16 +29,18 @@ import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import { useAppContext } from '@/context/app-context'
import { useExternalApiPanel } from '@/context/external-api-panel-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
const Container = () => {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const router = useRouter()
const { currentWorkspace, isCurrentWorkspaceOwner } = useAppContext()
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
const { showExternalApiPanel, setShowExternalApiPanel } = useExternalApiPanel()
const [includeAll, { toggle: toggleIncludeAll }] = useBoolean(false)
document.title = `${t('dataset.knowledge')} - Dify`
useDocumentTitle(t('dataset.knowledge'))
const options = useMemo(() => {
return [
@ -125,7 +127,7 @@ const Container = () => {
{activeTab === 'dataset' && (
<>
<Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} includeAll={includeAll} />
<DatasetFooter />
{!systemFeatures.branding.enabled && <DatasetFooter />}
{showTagManagementModal && (
<TagManagementModal type='knowledge' show={showTagManagementModal} />
)}

View File

@ -3,12 +3,12 @@
import { useCallback, useEffect, useRef } from 'react'
import useSWRInfinite from 'swr/infinite'
import { debounce } from 'lodash-es'
import { useTranslation } from 'react-i18next'
import NewDatasetCard from './NewDatasetCard'
import DatasetCard from './DatasetCard'
import type { DataSetListResponse, FetchDatasetsParams } from '@/models/datasets'
import { fetchDatasets } from '@/service/datasets'
import { useAppContext } from '@/context/app-context'
import { useTranslation } from 'react-i18next'
const getKey = (
pageIndex: number,
@ -48,6 +48,7 @@ const Datasets = ({
keywords,
includeAll,
}: Props) => {
const { t } = useTranslation()
const { isCurrentWorkspaceEditor } = useAppContext()
const { data, isLoading, setSize, mutate } = useSWRInfinite(
(pageIndex: number, previousPageData: DataSetListResponse) => getKey(pageIndex, previousPageData, tags, keywords, includeAll),
@ -57,11 +58,8 @@ const Datasets = ({
const loadingStateRef = useRef(false)
const anchorRef = useRef<HTMLAnchorElement>(null)
const { t } = useTranslation()
useEffect(() => {
loadingStateRef.current = isLoading
document.title = `${t('dataset.knowledge')} - Dify`
}, [isLoading, t])
const onScroll = useCallback(
@ -87,7 +85,7 @@ const Datasets = ({
return (
<nav className='grid shrink-0 grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>
{ isCurrentWorkspaceEditor && <NewDatasetCard ref={anchorRef} /> }
{isCurrentWorkspaceEditor && <NewDatasetCard ref={anchorRef} />}
{data?.map(({ data: datasets }) => datasets.map(dataset => (
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={mutate} />),
))}

View File

@ -1,6 +1,11 @@
'use client'
import { useTranslation } from 'react-i18next'
import Container from './Container'
import useDocumentTitle from '@/hooks/use-document-title'
const AppList = async () => {
const AppList = () => {
const { t } = useTranslation()
useDocumentTitle(t('common.menus.datasets'))
return <Container />
}

View File

@ -11,7 +11,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<div>
### Authentication
Service API of Dify authenticates using an `API-Key`.
Service API authenticates using an `API-Key`.
It is suggested that developers store the `API-Key` in the backend instead of sharing or storing it in the client side to avoid the leakage of the `API-Key`, which may lead to property loss.

View File

@ -11,7 +11,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<div>
### 鉴权
Dify Service API 使用 `API-Key` 进行鉴权。
Service API 使用 `API-Key` 进行鉴权。
建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。

View File

@ -1,11 +1,13 @@
import type { FC } from 'react'
'use client'
import type { FC, PropsWithChildren } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import ExploreClient from '@/app/components/explore'
export type IAppDetail = {
children: React.ReactNode
}
import useDocumentTitle from '@/hooks/use-document-title'
const AppDetail: FC<IAppDetail> = ({ children }) => {
const ExploreLayout: FC<PropsWithChildren> = ({ children }) => {
const { t } = useTranslation()
useDocumentTitle(t('common.menus.explore'))
return (
<ExploreClient>
{children}
@ -13,4 +15,4 @@ const AppDetail: FC<IAppDetail> = ({ children }) => {
)
}
export default React.memo(AppDetail)
export default React.memo(ExploreLayout)

View File

@ -30,9 +30,4 @@ const Layout = ({ children }: { children: ReactNode }) => {
</>
)
}
export const metadata = {
title: 'Dify',
}
export default Layout

View File

@ -1,22 +1,16 @@
'use client'
import type { FC } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import ToolProviderList from '@/app/components/tools/provider-list'
import { useAppContext } from '@/context/app-context'
const Layout: FC = () => {
const { t } = useTranslation()
import useDocumentTitle from '@/hooks/use-document-title'
const ToolsList: FC = () => {
const router = useRouter()
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
useEffect(() => {
if (typeof window !== 'undefined')
document.title = `${t('tools.title')} - Dify`
if (isCurrentWorkspaceDatasetOperator)
return router.replace('/datasets')
}, [isCurrentWorkspaceDatasetOperator, router, t])
const { t } = useTranslation()
useDocumentTitle(t('common.menus.tools'))
useEffect(() => {
if (isCurrentWorkspaceDatasetOperator)
@ -25,4 +19,4 @@ const Layout: FC = () => {
return <ToolProviderList />
}
export default React.memo(Layout)
export default React.memo(ToolsList)

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 { useGlobalPublicStore } from '@/context/global-public-context'
import { SSOProtocol } from '@/types/feature'
import Loading from '@/app/components/base/loading'
import AppUnavailable from '@/app/components/base/app-unavailable'
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,48 +47,47 @@ const WebSSOForm: FC = () => {
await setAccessToken(appCode, tokenFromUrl)
router.push(redirectUrl)
}
}, [getAppCodeFromRedirectUrl, redirectUrl, router, tokenFromUrl])
const handleSSOLogin = async (protocol: string) => {
const handleSSOLogin = useCallback(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
}
case '':
break
default:
showErrorToast('SSO protocol is not supported.')
}
}
}, [getAppCodeFromRedirectUrl, redirectUrl, router, systemFeatures.webapp_auth.sso_config.protocol])
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)
await handleSSOLogin()
return
}
@ -89,15 +95,45 @@ const WebSSOForm: FC = () => {
}
init()
}, [message, tokenFromUrl]) // Added dependencies to useEffect
}, [message, processTokenAndRedirect, tokenFromUrl, handleSSOLogin])
if (tokenFromUrl)
return <div className='flex h-full items-center justify-center'><Loading /></div>
if (message) {
return <div className='flex h-full items-center justify-center'>
<AppUnavailable code={'App Unavailable'} unknownReason={message} />
</div>
}
return (
<div className="flex h-full items-center justify-center">
<div className={cn('flex w-full grow flex-col items-center 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 h-full items-center justify-center">
<div className={cn('flex w-full grow flex-col items-center justify-center', 'px-6', 'md:px-[108px]')}>
<Loading />
</div>
</div>
)
}
return <div className="flex h-full items-center justify-center">
<div className="rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2 p-4">
<div className='shadows-shadow-lg mb-2 flex h-10 w-10 items-center justify-center rounded-xl bg-components-card-bg shadow'>
<RiDoorLockLine className='h-5 w-5' />
</div>
<p className='system-sm-medium text-text-primary'>{t('login.webapp.noLoginMethod')}</p>
<p className='system-xs-regular mt-1 text-text-tertiary'>{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='h-px w-full bg-gradient-to-r from-background-gradient-mask-transparent via-divider-regular to-background-gradient-mask-transparent'></div>
</div>
</div>
</div>
)
}
else {
return <div className="flex h-full items-center justify-center">
<p className='system-xs-regular text-text-tertiary'>{t('login.webapp.disabled')}</p>
</div>
}
}
export default React.memo(WebSSOForm)

View File

@ -20,6 +20,7 @@ import AppIcon from '@/app/components/base/app-icon'
import { IS_CE_EDITION } from '@/config'
import Input from '@/app/components/base/input'
import PremiumBadge from '@/app/components/base/premium-badge'
import { useGlobalPublicStore } from '@/context/global-public-context'
const titleClassName = `
system-sm-semibold text-text-secondary
@ -32,7 +33,7 @@ const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
export default function AccountPage() {
const { t } = useTranslation()
const { systemFeatures } = useAppContext()
const { systemFeatures } = useGlobalPublicStore()
const { mutateUserProfile, userProfile, apps } = useAppContext()
const { isEducationAccount } = useProviderContext()
const { notify } = useContext(ToastContext)
@ -138,7 +139,7 @@ export default function AccountPage() {
<h4 className='title-2xl-semi-bold text-text-primary'>{t('common.account.myAccount')}</h4>
</div>
<div className='mb-8 flex items-center rounded-xl bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 p-6'>
<AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={ mutateUserProfile } size={64} />
<AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={mutateUserProfile} size={64} />
<div className='ml-4'>
<p className='system-xl-semibold text-text-primary'>
{userProfile.name}

View File

@ -32,9 +32,4 @@ const Layout = ({ children }: { children: ReactNode }) => {
</>
)
}
export const metadata = {
title: 'Dify',
}
export default Layout

View File

@ -1,6 +1,11 @@
'use client'
import { useTranslation } from 'react-i18next'
import AccountPage from './account-page'
import useDocumentTitle from '@/hooks/use-document-title'
export default function Account() {
const { t } = useTranslation()
useDocumentTitle(t('common.menus.account'))
return <div className='mx-auto w-full max-w-[640px] px-6 pt-12'>
<AccountPage />
</div>

View File

@ -7,8 +7,10 @@ import Button from '@/app/components/base/button'
import { invitationCheck } from '@/service/common'
import Loading from '@/app/components/base/loading'
import useDocumentTitle from '@/hooks/use-document-title'
const ActivateForm = () => {
useDocumentTitle('')
const router = useRouter()
const { t } = useTranslation()
const searchParams = useSearchParams()

View File

@ -1,17 +1,20 @@
'use client'
import React from 'react'
import Header from '../signin/_header'
import ActivateForm from './activateForm'
import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Activate = () => {
const { systemFeatures } = useGlobalPublicStore()
return (
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
<Header />
<ActivateForm />
<div className='px-8 py-6 text-sm font-normal text-text-tertiary'>
{!systemFeatures.branding.enabled && <div className='px-8 py-6 text-sm font-normal text-text-tertiary'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div>
</div>}
</div>
</div>
)

View File

@ -34,6 +34,7 @@ import { fetchWorkflowDraft } from '@/service/workflow'
import ContentDialog from '@/app/components/base/content-dialog'
import Button from '@/app/components/base/button'
import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView'
import Divider from '../base/divider'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../base/portal-to-follow-elem'
export type IAppInfoProps = {
@ -270,8 +271,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
onClick={() => {
setOpen(false)
setShowDuplicateModal(true)
}}
>
}}>
<RiFileCopy2Line className='h-3.5 w-3.5 text-components-button-secondary-text' />
<span className='system-xs-medium text-components-button-secondary-text'>{t('app.duplicate')}</span>
</Button>
@ -337,6 +337,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
className='flex grow flex-col gap-2 overflow-auto px-2 py-1'
/>
</div>
<Divider />
<div className='flex min-h-fit shrink-0 flex-col items-start justify-center gap-3 self-stretch border-t-[0.5px] border-divider-subtle p-2'>
<Button
size={'medium'}

View File

@ -16,7 +16,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('relative h-auto min-h-[323px] w-[600px] overflow-y-auto rounded-2xl bg-components-panel-bg p-0 shadow-xl transition-all', className)}>
<div onClick={() => close()} className="absolute right-5 top-5 z-10 flex h-8 w-8 cursor-pointer items-center justify-center">
<RiCloseLine className='h-5 w-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="cursor-pointer rounded-[10px] border-[1px]
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,204 @@
'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 { FloatingOverlay } from '@floating-ui/react'
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 { isLoading, 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 && !isLoading && hasMore)
fetchNextPage()
}, { rootMargin: '20px' })
observer.observe(anchorRef.current)
}
return () => observer?.disconnect()
}, [isLoading, fetchNextPage, anchorRef, data])
return <PortalToFollowElem open={open} onOpenChange={setOpen} offset={{ crossAxis: 300 }} placement='bottom-end'>
<PortalToFollowElemTrigger asChild>
<Button variant='ghost-accent' size='small' className='flex shrink-0 items-center gap-x-0.5' onClick={() => setOpen(!open)}>
<RiAddCircleFill className='h-4 w-4' />
<span>{t('common.operation.add')}</span>
</Button>
</PortalToFollowElemTrigger>
{open && <FloatingOverlay />}
<PortalToFollowElemContent className='z-[25]'>
<div className='relative flex max-h-[400px] w-[400px] flex-col overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[5px]'>
<div className='sticky top-0 z-10 bg-components-panel-bg-blur p-2 pb-0.5 backdrop-blur-[5px]'>
<Input value={keyword} onChange={handleKeywordChange} showLeftIcon placeholder={t('app.accessControlDialog.operateGroupAndMember.searchPlaceholder') as string} />
</div>
{
isLoading
? <div className='p-1'><Loading /></div>
: (data?.pages?.length ?? 0) > 0
? <>
<div className='flex h-7 items-center 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 h-7 items-center justify-center 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 h-7 items-center gap-x-0.5 px-2 py-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='system-xs-regular flex items-center gap-x-0.5 text-text-tertiary'>
<span>/</span>
<span className={index === selectedGroupsForBreadcrumb.length - 1 ? '' : 'cursor-pointer text-text-accent'} 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='h-4 w-4 shrink-0' onCheck={handleCheckChange} />
<div className='item-center flex grow'>
<div className='mr-2 h-5 w-5 overflow-hidden rounded-full bg-components-icon-bg-blue-solid'>
<div className='bg-access-app-icon-mask-bg flex h-full w-full items-center justify-center'>
<RiOrganizationChart className='h-[14px] w-[14px] text-components-avatar-shape-fill-stop-0' />
</div>
</div>
<p className='system-sm-medium mr-1 text-text-secondary'>{group.name}</p>
<p className='system-xs-regular text-text-tertiary'>{group.groupSize}</p>
</div>
<Button size="small" disabled={isChecked} variant='ghost-accent'
className='flex shrink-0 items-center justify-between px-1.5 py-1' onClick={handleExpandClick}>
<span className='px-[3px]'>{t('app.accessControlDialog.operateGroupAndMember.expand')}</span>
<RiArrowRightSLine className='h-4 w-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='h-4 w-4 shrink-0' onCheck={handleCheckChange} />
<div className='flex grow items-center'>
<div className='mr-2 h-5 w-5 overflow-hidden rounded-full bg-components-icon-bg-blue-solid'>
<div className='bg-access-app-icon-mask-bg flex h-full w-full items-center justify-center'>
<Avatar className='h-[14px] w-[14px]' textClassName='text-[12px]' avatar={null} name={member.name} />
</div>
</div>
<p className='system-sm-medium mr-1 text-text-secondary'>{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='pb-3 pl-6 pr-14 pt-6'>
<Dialog.Title className='title-2xl-semi-bold text-text-primary'>{t('app.accessControlDialog.title')}</Dialog.Title>
<Dialog.Description className='system-xs-regular mt-1 text-text-tertiary'>{t('app.accessControlDialog.description')}</Dialog.Description>
</div>
<div className='flex flex-col gap-y-1 px-6 pb-3'>
<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='flex grow items-center gap-x-2'>
<RiBuildingLine className='h-4 w-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 gap-x-2 p-3'>
<RiGlobalLine className='h-4 w-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 gap-x-2 p-6 pt-5'>
<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='flex grow items-center gap-x-2'>
<RiLockLine className='h-4 w-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='flex grow items-center gap-x-1'>
<RiLockLine className='h-4 w-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='ml-2 mr-0 h-[14px]' type="vertical" />
</>}
<AddMemberOrGroupDialog />
</div>
</div>
<div className='px-1 pb-1'>
<div className='flex max-h-[400px] flex-col gap-y-2 overflow-y-auto rounded-lg bg-background-section p-2'>
{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 pb-1.5 pt-5'><p className='system-xs-regular text-center text-text-tertiary'>{t('app.accessControlDialog.noGroupsOrMembers')}</p></div>
return <>
<p className='system-2xs-medium-uppercase sticky top-0 text-text-tertiary'>{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 sticky top-0 text-text-tertiary'>{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='h-[14px] w-[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='h-[14px] w-[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='group flex flex-row items-center gap-x-1 rounded-full border-[0.5px] bg-components-badge-white-to-dark p-1 pr-1.5 shadow-xs'>
<div className='h-5 w-5 overflow-hidden rounded-full bg-components-icon-bg-blue-solid'>
<div className='bg-access-app-icon-mask-bg flex h-full w-full items-center justify-center'>
{icon}
</div>
</div>
{children}
<div className='flex h-4 w-4 cursor-pointer items-center justify-center' onClick={onRemove}>
<RiCloseCircleFill className='h-[14px] w-[14px] text-text-quaternary' />
</div>
</div>
}
export function WebAppSSONotEnabledTip() {
const { t } = useTranslation()
return <Tooltip asChild={false} popupContent={t('app.accessControlDialog.webAppSSONotEnabledTip')}>
<RiAlertFill className='h-4 w-4 shrink-0 text-text-warning-secondary' />
</Tooltip>
}

View File

@ -1,21 +1,28 @@
import {
memo,
useCallback,
useEffect,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import dayjs from 'dayjs'
import {
RiArrowDownSLine,
RiArrowRightSLine,
RiLockLine,
RiPlanetLine,
RiPlayCircleLine,
RiPlayList2Line,
RiTerminalBoxLine,
} from '@remixicon/react'
import { useKeyPress } from 'ahooks'
import { getKeyboardKeyCodeBySystem } from '../../workflow/utils'
import Toast from '../../base/toast'
import type { ModelAndParameter } from '../configuration/debug/types'
import { getKeyboardKeyCodeBySystem } from '../../workflow/utils'
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'
@ -34,6 +41,10 @@ import WorkflowToolConfigureButton from '@/app/components/tools/workflow-tool/co
import type { InputVar } from '@/app/components/workflow/types'
import { appDefaultIconBackground } from '@/config'
import type { PublishWorkflowParams } from '@/types/workflow'
import { useAppWhiteListSubjects, useGetUserCanAccessApp } from '@/service/access-control'
import { AccessMode } from '@/models/access-control'
import { fetchAppDetail } from '@/service/apps'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type AppPublisherProps = {
disabled?: boolean
@ -74,11 +85,33 @@ 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 systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
const appMode = (appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow') ? 'chat' : appDetail.mode
const appURL = `${appBaseURL}${basePath}/${appMode}/${accessToken}`
const isChatApp = ['chat', 'agent-chat', 'completion'].includes(appDetail?.mode || '')
const { data: userCanAccessApp, isLoading: isGettingUserCanAccessApp, refetch } = useGetUserCanAccessApp({ appId: appDetail?.id, enabled: false })
const { data: appAccessSubjects, isLoading: isGettingAppWhiteListSubjects } = useAppWhiteListSubjects(appDetail?.id, open && systemFeatures.webapp_auth.enabled && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS)
useEffect(() => {
if (systemFeatures.webapp_auth.enabled && open && appDetail)
refetch()
}, [open, appDetail, refetch, systemFeatures])
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()
@ -99,7 +132,7 @@ const AppPublisher = ({
await onRestore?.()
setOpen(false)
}
catch {}
catch { }
}, [onRestore])
const handleTrigger = useCallback(() => {
@ -130,6 +163,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)
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.shift.p`, (e) => {
@ -138,7 +178,7 @@ const AppPublisher = ({
return
handlePublish()
},
{ exactMatch: true, useCapture: true })
{ exactMatch: true, useCapture: true })
return (
<>
@ -223,70 +263,105 @@ const AppPublisher = ({
)
}
</div>
<div className='border-t-[0.5px] border-t-divider-regular p-4 pt-3'>
<SuggestedAction
disabled={!publishedAt}
link={appURL}
icon={<RiPlayCircleLine className='h-4 w-4' />}
>
{t('workflow.common.runApp')}
</SuggestedAction>
{appDetail?.mode === 'workflow' || appDetail?.mode === 'completion'
? (
<SuggestedAction
disabled={!publishedAt}
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
icon={<RiPlayList2Line className='h-4 w-4' />}
>
{t('workflow.common.batchRunApp')}
</SuggestedAction>
)
: (
<SuggestedAction
{(systemFeatures.webapp_auth.enabled && (isGettingUserCanAccessApp || isGettingAppWhiteListSubjects))
? <div className='py-2'><Loading /></div>
: <>
<Divider className='my-0' />
{systemFeatures.webapp_auth.enabled && <div className='p-4 pt-3'>
<div className='flex h-6 items-center'>
<p className='system-xs-medium text-text-tertiary'>{t('app.publishApp.title')}</p>
</div>
<div className='flex h-8 cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal py-1 pl-2.5 pr-2 hover:bg-primary-50 hover:text-text-accent'
onClick={() => {
setEmbeddingModalOpen(true)
handleTrigger()
}}
setShowAppAccessControl(true)
}}>
<div className='flex grow items-center gap-x-1.5 pr-1'>
<RiLockLine className='h-4 w-4 shrink-0 text-text-secondary' />
{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='system-xs-regular shrink-0 text-text-tertiary'>{t('app.publishApp.notSet')}</p>}
<div className='flex h-4 w-4 shrink-0 items-center justify-center'>
<RiArrowRightSLine className='h-4 w-4 text-text-quaternary' />
</div>
</div>
{!isAppAccessSet && <p className='system-xs-regular mt-1 text-text-warning'>{t('app.publishApp.notSetDesc')}</p>}
</div>}
<div className='flex flex-col gap-y-1 border-t-[0.5px] border-t-divider-regular p-4 pt-3'>
<Tooltip triggerClassName='flex' disabled={!systemFeatures.webapp_auth.enabled || userCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}>
<SuggestedAction
className='flex-1'
disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && !userCanAccessApp?.result)}
link={appURL}
icon={<RiPlayCircleLine className='h-4 w-4' />}
>
{t('workflow.common.runApp')}
</SuggestedAction>
</Tooltip>
{appDetail?.mode === 'workflow' || appDetail?.mode === 'completion'
? (
<Tooltip triggerClassName='flex' disabled={!systemFeatures.webapp_auth.enabled || userCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}>
<SuggestedAction
className='flex-1'
disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && !userCanAccessApp?.result)}
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
icon={<RiPlayList2Line className='h-4 w-4' />}
>
{t('workflow.common.batchRunApp')}
</SuggestedAction>
</Tooltip>
)
: (
<SuggestedAction
onClick={() => {
setEmbeddingModalOpen(true)
handleTrigger()
}}
disabled={!publishedAt}
icon={<CodeBrowser className='h-4 w-4' />}
>
{t('workflow.common.embedIntoSite')}
</SuggestedAction>
)}
<Tooltip triggerClassName='flex' disabled={!systemFeatures.webapp_auth.enabled || userCanAccessApp?.result} popupContent={t('app.noAccessPermission')} asChild={false}>
<SuggestedAction
className='flex-1'
onClick={() => {
publishedAt && handleOpenInExplore()
}}
disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && !userCanAccessApp?.result)}
icon={<RiPlanetLine className='h-4 w-4' />}
>
{t('workflow.common.openInExplore')}
</SuggestedAction>
</Tooltip>
<SuggestedAction
disabled={!publishedAt}
icon={<CodeBrowser className='h-4 w-4' />}
link='./develop'
icon={<RiTerminalBoxLine className='h-4 w-4' />}
>
{t('workflow.common.embedIntoSite')}
{t('workflow.common.accessAPIReference')}
</SuggestedAction>
)}
<SuggestedAction
onClick={() => {
publishedAt && handleOpenInExplore()
}}
disabled={!publishedAt}
icon={<RiPlanetLine className='h-4 w-4' />}
>
{t('workflow.common.openInExplore')}
</SuggestedAction>
<SuggestedAction
disabled={!publishedAt}
link='./develop'
icon={<RiTerminalBoxLine className='h-4 w-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>
{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>
</>}
</div>
</PortalToFollowElemContent>
<EmbeddedModal
@ -296,9 +371,9 @@ const AppPublisher = ({
appBaseUrl={appBaseURL}
accessToken={accessToken}
/>
{showAppAccessControl && <AccessControl app={appDetail!} onConfirm={handleAccessControlUpdate} onClose={() => { setShowAppAccessControl(false) }} />}
</PortalToFollowElem >
</>
)
</>)
}
export default memo(AppPublisher)

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 py-2 px-2.5 bg-background-section-burn rounded-lg text-text-secondary transition-colors [&:not(:first-child)]:mt-1',
disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'hover:bg-state-accent-hover hover:text-text-accent cursor-pointer',
className,
)}
{...props}
>
<div className='relative h-4 w-4'>{icon}</div>
<div className='system-sm-medium shrink grow basis-0'>{children}</div>
<RiArrowRightUpLine className='h-3.5 w-3.5' />
</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 justify-start items-center gap-2 py-2 px-2.5 bg-background-section-burn rounded-lg text-text-secondary transition-colors [&:not(:first-child)]:mt-1',
disabled ? 'shadow-xs opacity-30 cursor-not-allowed' : 'text-text-secondary hover:bg-state-accent-hover hover:text-text-accent cursor-pointer',
className,
)}
onClick={handleClick}
{...props}
>
<div className='relative h-4 w-4'>{icon}</div>
<div className='system-sm-medium shrink grow basis-0'>{children}</div>
<RiArrowRightUpLine className='h-3.5 w-3.5' />
</a>
)
}
export default SuggestedAction

View File

@ -1,11 +1,13 @@
'use client'
import React, { useMemo, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { usePathname, useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import {
RiArrowRightSLine,
RiBookOpenLine,
RiEqualizer2Line,
RiExternalLinkLine,
RiLockLine,
RiPaintBrushLine,
RiWindowLine,
} from '@remixicon/react'
@ -18,6 +20,7 @@ import Tooltip from '@/app/components/base/tooltip'
import AppBasic from '@/app/components/app-sidebar/basic'
import { asyncRunSafe, randomString } from '@/utils'
import { basePath } from '@/utils/var'
import { useStore as useAppStore } from '@/app/components/app/store'
import Button from '@/app/components/base/button'
import Switch from '@/app/components/base/switch'
import Divider from '@/app/components/base/divider'
@ -29,6 +32,11 @@ import type { AppDetailResponse } from '@/models/app'
import { useAppContext } from '@/context/app-context'
import type { AppSSO } from '@/types/app'
import Indicator from '@/app/components/header/indicator'
import { fetchAppDetail } from '@/service/apps'
import { AccessMode } from '@/models/access-control'
import AccessControl from '../app-access-control'
import { useAppWhiteListSubjects } from '@/service/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type IAppCardProps = {
className?: string
@ -54,13 +62,17 @@ function AppCard({
const router = useRouter()
const pathname = usePathname()
const { isCurrentWorkspaceManager, isCurrentWorkspaceEditor } = useAppContext()
const appDetail = useAppStore(state => state.appDetail)
const setAppDetail = useAppStore(state => state.setAppDetail)
const [showSettingsModal, setShowSettingsModal] = useState(false)
const [showEmbedded, setShowEmbedded] = useState(false)
const [showCustomizeModal, setShowCustomizeModal] = useState(false)
const [genLoading, setGenLoading] = useState(false)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [showAccessControl, setShowAccessControl] = useState<boolean>(false)
const { t } = useTranslation()
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const { data: appAccessSubjects } = useAppWhiteListSubjects(appDetail?.id, systemFeatures.webapp_auth.enabled && appDetail?.access_mode === AccessMode.SPECIFIC_GROUPS_MEMBERS)
const OPERATIONS_MAP = useMemo(() => {
const operationsMap = {
@ -128,6 +140,31 @@ function AppCard({
}
}
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 handleClickAccessControl = useCallback(() => {
if (!appDetail)
return
setShowAccessControl(true)
}, [appDetail])
const handleAccessControlUpdate = useCallback(() => {
fetchAppDetail({ url: '/apps', id: appDetail!.id }).then((res) => {
setAppDetail(res)
setShowAccessControl(false)
})
}, [appDetail, setAppDetail])
return (
<div
className={
@ -206,6 +243,22 @@ function AppCard({
)}
</div>
</div>
{isApp && systemFeatures.webapp_auth.enabled && appDetail && <div className='flex flex-col items-start justify-center self-stretch'>
<div className="system-xs-medium pb-1 text-text-tertiary">{t('app.publishApp.title')}</div>
<div className='flex h-9 w-full cursor-pointer items-center gap-x-0.5 rounded-lg bg-components-input-bg-normal py-1 pl-2.5 pr-2'
onClick={handleClickAccessControl}>
<div className='flex grow items-center gap-x-1.5 pr-1'>
<RiLockLine className='h-4 w-4 shrink-0 text-text-secondary' />
{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='system-xs-regular shrink-0 text-text-tertiary'>{t('app.publishApp.notSet')}</p>}
<div className='flex h-4 w-4 shrink-0 items-center justify-center'>
<RiArrowRightSLine className='h-4 w-4 text-text-quaternary' />
</div>
</div>
</div>}
</div>
<div className={'flex items-center gap-1 self-stretch p-3'}>
{!isApp && <SecretKeyButton appId={appInfo.id} />}
@ -264,6 +317,11 @@ function AppCard({
api_base_url={appInfo.api_base_url}
mode={appInfo.mode}
/>
{
showAccessControl && <AccessControl app={appDetail!}
onConfirm={handleAccessControlUpdate}
onClose={() => { setShowAccessControl(false) }} />
}
</>
)
: null}

View File

@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useState } from 'react'
import { RiArrowRightSLine, RiCloseLine } from '@remixicon/react'
import Link from 'next/link'
import { Trans, useTranslation } from 'react-i18next'
import { useContext, useContextSelector } from 'use-context-selector'
import { useContext } from 'use-context-selector'
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
import Modal from '@/app/components/base/modal'
import ActionButton from '@/app/components/base/action-button'
@ -21,7 +21,6 @@ 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 AppContext, { 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'
@ -65,8 +64,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
onClose,
onSave,
}) => {
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
const { isCurrentWorkspaceEditor } = useAppContext()
const { notify } = useToastContext()
const [isShowMore, setIsShowMore] = useState(false)
const {
@ -110,7 +107,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
: { type: 'emoji', icon, background: icon_background! },
)
const { enableBilling, plan } = useProviderContext()
const { enableBilling, plan, webappCopyrightEnabled } = useProviderContext()
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
const isFreePlan = plan.type === 'sandbox'
const handlePlanClick = useCallback(() => {
@ -138,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()
@ -188,7 +185,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
chat_color_theme: inputInfo.chatColorTheme,
chat_color_theme_inverted: inputInfo.chatColorThemeInverted,
prompt_public: false,
copyright: isFreePlan
copyright: !webappCopyrightEnabled
? ''
: inputInfo.copyrightSwitchValue
? inputInfo.copyright
@ -336,28 +333,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
</div>
<p className='body-xs-regular pb-0.5 text-text-tertiary'>{t(`${prefixSettings}.workflow.showDesc`)}</p>
</div>
{/* SSO */}
{systemFeatures.enable_web_sso_switch_component && (
<>
<Divider className="my-0 h-px" />
<div className='w-full'>
<p className='system-xs-medium-uppercase mb-1 text-text-tertiary'>{t(`${prefixSettings}.sso.label`)}</p>
<div className='flex items-center justify-between'>
<div className={cn('system-sm-semibold py-1 text-text-secondary')}>{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='body-xs-regular pb-0.5 text-text-tertiary'>{t(`${prefixSettings}.sso.description`)}</p>
</div>
</>
)}
{/* more settings switch */}
<Divider className="my-0 h-px" />
{!isShowMore && (
@ -392,14 +367,14 @@ const SettingsModal: FC<ISettingsModalProps> = ({
)}
</div>
<Tooltip
disabled={!isFreePlan}
disabled={webappCopyrightEnabled}
popupContent={
<div className='w-[260px]'>{t(`${prefixSettings}.more.copyrightTooltip`)}</div>
<div className='w-[180px]'>{t(`${prefixSettings}.more.copyrightTooltip`)}</div>
}
asChild={false}
>
<Switch
disabled={isFreePlan}
disabled={!webappCopyrightEnabled}
defaultValue={inputInfo.copyrightSwitchValue}
onChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
/>
@ -450,7 +425,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
<Button className='mr-2' onClick={onHide}>{t('common.operation.cancel')}</Button>
<Button variant='primary' onClick={onClickSave} loading={saveLoading}>{t('common.operation.save')}</Button>
</div>
{showAppIconPicker && (
<div onClick={e => e.stopPropagation()}>
<AppIconPicker

View File

@ -4,7 +4,7 @@ import React from 'react'
import { useTranslation } from 'react-i18next'
type IAppUnavailableProps = {
code?: number
code?: number | string
isUnknownReason?: boolean
unknownReason?: string
}

View File

@ -16,12 +16,15 @@ import type {
ConversationItem,
} from '@/models/share'
import { noop } from 'lodash-es'
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
@ -60,6 +63,8 @@ export type ChatWithHistoryContextValue = {
}
export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
accessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
userCanAccess: false,
currentConversationId: '',
appPrevChatTree: [],
pinnedConversationList: [],

View File

@ -43,6 +43,9 @@ import { useAppFavicon } from '@/hooks/use-app-favicon'
import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import { noop } from 'lodash-es'
import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { AccessMode } from '@/models/access-control'
function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = []
@ -72,7 +75,18 @@ function getFormattedChatList(messages: any[]) {
export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
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,
enabled: systemFeatures.webapp_auth.enabled,
})
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({
appId: installedAppInfo?.app.id || appInfo?.app_id,
isInstalledApp,
enabled: systemFeatures.webapp_auth.enabled,
})
useAppFavicon({
enable: !installedAppInfo,
@ -447,7 +461,9 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
return {
appInfoError,
appInfoLoading,
appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission)),
accessMode: systemFeatures.webapp_auth.enabled ? appAccessMode?.accessMode : AccessMode.PUBLIC,
userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true,
isInstalledApp,
appId,
currentConversationId,

View File

@ -20,6 +20,7 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { checkOrSetAccessToken } from '@/app/components/share/utils'
import AppUnavailable from '@/app/components/base/app-unavailable'
import cn from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title'
type ChatWithHistoryProps = {
className?: string
@ -28,6 +29,7 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
className,
}) => {
const {
userCanAccess,
appInfoError,
appData,
appInfoLoading,
@ -45,19 +47,17 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
useEffect(() => {
themeBuilder?.buildTheme(site?.chat_color_theme, site?.chat_color_theme_inverted)
if (site) {
if (customConfig)
document.title = `${site.title}`
else
document.title = `${site.title} - Powered by Dify`
}
}, [site, customConfig, themeBuilder])
useDocumentTitle(site?.title || 'Chat')
if (appInfoLoading) {
return (
<Loading type='app' />
)
}
if (!userCanAccess)
return <AppUnavailable code={403} unknownReason='no permission.' />
if (appInfoError) {
return (
@ -124,6 +124,8 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
const {
appInfoError,
appInfoLoading,
accessMode,
userCanAccess,
appData,
appParams,
appMeta,
@ -166,6 +168,8 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
appInfoError,
appInfoLoading,
appData,
accessMode,
userCanAccess,
appParams,
appMeta,
appChatListDataLoading,

View File

@ -19,6 +19,8 @@ import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/re
import DifyLogo from '@/app/components/base/logo/dify-logo'
import type { ConversationItem } from '@/models/share'
import cn from '@/utils/classnames'
import { AccessMode } from '@/models/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context'
type Props = {
isPanel?: boolean
@ -27,6 +29,8 @@ type Props = {
const Sidebar = ({ isPanel }: Props) => {
const { t } = useTranslation()
const {
isInstalledApp,
accessMode,
appData,
handleNewConversation,
pinnedConversationList,
@ -44,7 +48,7 @@ const Sidebar = ({ isPanel }: Props) => {
isResponding,
} = useChatWithHistoryContext()
const isSidebarCollapsed = sidebarCollapseState
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const [showConfirm, setShowConfirm] = useState<ConversationItem | null>(null)
const [showRename, setShowRename] = useState<ConversationItem | null>(null)
@ -136,7 +140,7 @@ const Sidebar = ({ isPanel }: Props) => {
)}
</div>
<div className='flex shrink-0 items-center justify-between p-3'>
<MenuDropdown placement='top-start' data={appData?.site} />
<MenuDropdown hideLogout={isInstalledApp || accessMode === AccessMode.PUBLIC} placement='top-start' data={appData?.site} />
{/* powered by */}
<div className='shrink-0'>
{!appData?.custom_config?.remove_webapp_brand && (
@ -144,34 +148,33 @@ const Sidebar = ({ isPanel }: Props) => {
'flex shrink-0 items-center gap-1.5 px-1',
)}>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{t('share.chat.poweredBy')}</div>
{appData?.custom_config?.replace_webapp_logo && (
<img src={appData?.custom_config?.replace_webapp_logo} alt='logo' className='block h-5 w-auto' />
)}
{!appData?.custom_config?.replace_webapp_logo && (
<DifyLogo size='small' />
)}
{systemFeatures.branding.enabled ? (
<img src={systemFeatures.branding.login_page_logo} alt='logo' className='block h-5 w-auto' />
) : (
<DifyLogo size='small' />)
}
</div>
)}
</div>
{!!showConfirm && (
<Confirm
title={t('share.chat.deleteConversation.title')}
content={t('share.chat.deleteConversation.content') || ''}
isShow
onCancel={handleCancelConfirm}
onConfirm={handleDelete}
/>
)}
{showRename && (
<RenameModal
isShow
onClose={handleCancelRename}
saveLoading={conversationRenaming}
name={showRename?.name || ''}
onSave={handleRename}
/>
)}
</div>
{!!showConfirm && (
<Confirm
title={t('share.chat.deleteConversation.title')}
content={t('share.chat.deleteConversation.content') || ''}
isShow
onCancel={handleCancelConfirm}
onConfirm={handleDelete}
/>
)}
{showRename && (
<RenameModal
isShow
onClose={handleCancelRename}
saveLoading={conversationRenaming}
name={showRename?.name || ''}
onSave={handleRename}
/>
)}
</div>
)
}

View File

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

View File

@ -36,6 +36,9 @@ import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import { noop } from 'lodash-es'
import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { AccessMode } from '@/models/access-control'
function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = []
@ -65,7 +68,18 @@ function getFormattedChatList(messages: any[]) {
export const useEmbeddedChatbot = () => {
const isInstalledApp = false
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', fetchAppInfo)
const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({
appId: appInfo?.app_id,
isInstalledApp,
enabled: systemFeatures.webapp_auth.enabled,
})
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({
appId: appInfo?.app_id,
isInstalledApp,
enabled: systemFeatures.webapp_auth.enabled,
})
const appData = useMemo(() => {
return appInfo
@ -364,7 +378,9 @@ export const useEmbeddedChatbot = () => {
return {
appInfoError,
appInfoLoading,
appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission)),
accessMode: systemFeatures.webapp_auth.enabled ? appAccessMode?.accessMode : AccessMode.PUBLIC,
userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true,
isInstalledApp,
allowResetChat,
appId,

View File

@ -21,9 +21,11 @@ import Header from '@/app/components/base/chat/embedded-chatbot/header'
import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import cn from '@/utils/classnames'
import useDocumentTitle from '@/hooks/use-document-title'
const Chatbot = () => {
const {
userCanAccess,
isMobile,
allowResetChat,
appInfoError,
@ -43,14 +45,10 @@ const Chatbot = () => {
useEffect(() => {
themeBuilder?.buildTheme(site?.chat_color_theme, site?.chat_color_theme_inverted)
if (site) {
if (customConfig)
document.title = `${site.title}`
else
document.title = `${site.title} - Powered by Dify`
}
}, [site, customConfig, themeBuilder])
useDocumentTitle(site?.title || 'Chat')
if (appInfoLoading) {
return (
<>
@ -66,6 +64,9 @@ const Chatbot = () => {
)
}
if (!userCanAccess)
return <AppUnavailable code={403} unknownReason='no permission.' />
if (appInfoError) {
return (
<>
@ -137,6 +138,8 @@ const EmbeddedChatbotWrapper = () => {
appInfoError,
appInfoLoading,
appData,
accessMode,
userCanAccess,
appParams,
appMeta,
appChatListDataLoading,
@ -168,6 +171,8 @@ const EmbeddedChatbotWrapper = () => {
} = useEmbeddedChatbot()
return <EmbeddedChatbotContext.Provider value={{
userCanAccess,
accessMode,
appInfoError,
appInfoLoading,
appData,

View File

@ -3,7 +3,7 @@ import type { FC } from 'react'
import classNames from '@/utils/classnames'
import useTheme from '@/hooks/use-theme'
import { basePath } from '@/utils/var'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type LogoStyle = 'default' | 'monochromeWhite'
export const logoPathMap: Record<LogoStyle, string> = {
@ -32,10 +32,15 @@ const DifyLogo: FC<DifyLogoProps> = ({
}) => {
const { theme } = useTheme()
const themedStyle = (theme === 'dark' && style === 'default') ? 'monochromeWhite' : style
const { systemFeatures } = useGlobalPublicStore()
let src = `${basePath}${logoPathMap[themedStyle]}`
if (systemFeatures.branding.enabled)
src = systemFeatures.branding.workspace_logo
return (
<img
src={`${basePath}${logoPathMap[themedStyle]}`}
src={src}
className={classNames('block object-contain', logoSizeMap[size], className)}
alt='Dify logo'
/>

View File

@ -1,7 +1,7 @@
import { useEffect, useRef, useState } from 'react'
import { SVG } from '@svgdotjs/svg.js'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
import DOMPurify from 'dompurify'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
export const SVGRenderer = ({ content }: { content: string }) => {
const svgRef = useRef<HTMLDivElement>(null)

View File

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

View File

@ -94,6 +94,11 @@ export type CurrentPlanInfoBackend = {
education: {
enabled: boolean
activated: boolean
},
webapp_copyright_enabled: boolean
workspace_members: {
size: number
limit: number
}
}

View File

@ -21,6 +21,7 @@ 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'
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
type IStepOneProps = {
datasetId?: string
dataSourceType?: DataSourceType
@ -45,7 +46,8 @@ type IStepOneProps = {
type NotionConnectorProps = {
onSetting: () => void
}
export const NotionConnector = ({ onSetting }: NotionConnectorProps) => {
export const NotionConnector = (props: NotionConnectorProps) => {
const { onSetting } = props
const { t } = useTranslation()
return (
@ -162,7 +164,7 @@ const StepOne = ({
>
<span className={cn(s.datasetIcon)} />
<span
title={t('datasetCreation.stepOne.dataSourceType.file')}
title={t('datasetCreation.stepOne.dataSourceType.file')!}
className='truncate'
>
{t('datasetCreation.stepOne.dataSourceType.file')}
@ -185,7 +187,7 @@ const StepOne = ({
>
<span className={cn(s.datasetIcon, s.notion)} />
<span
title={t('datasetCreation.stepOne.dataSourceType.notion')}
title={t('datasetCreation.stepOne.dataSourceType.notion')!}
className='truncate'
>
{t('datasetCreation.stepOne.dataSourceType.notion')}
@ -193,21 +195,21 @@ const StepOne = ({
</div>
{(ENABLE_WEBSITE_FIRECRAWL || ENABLE_WEBSITE_JINAREADER || ENABLE_WEBSITE_WATERCRAWL) && (
<div
className={cn(
s.dataSourceItem,
'system-sm-medium',
dataSourceType === DataSourceType.WEB && s.active,
dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled,
)}
onClick={() => changeType(DataSourceType.WEB)}
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>
<span className={cn(s.datasetIcon, s.web)} />
<span
title={t('datasetCreation.stepOne.dataSourceType.web')!}
className='truncate'
>
{t('datasetCreation.stepOne.dataSourceType.web')}
</span>
</div>
)}
</div>

View File

@ -15,7 +15,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
### 鉴权
Dify Service API 使用 `API-Key` 进行鉴权。
Service API 使用 `API-Key` 进行鉴权。
<i>**强烈建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。**</i>
所有 API 请求都应在 **`Authorization`** HTTP Header 中包含您的 `API-Key`,如下所示:

View File

@ -14,7 +14,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
### Authentication
Dify Service API 使用 `API-Key` 进行鉴权。
Service API 使用 `API-Key` 进行鉴权。
<i>**强烈建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。**</i>
所有 API 请求都应在 **`Authorization`** HTTP Header 中包含您的 `API-Key`,如下所示:

View File

@ -2,12 +2,13 @@
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import ExploreContext from '@/context/explore-context'
import Sidebar from '@/app/components/explore/sidebar'
import { useAppContext } from '@/context/app-context'
import { fetchMembers } from '@/service/common'
import type { InstalledApp } from '@/models/explore'
import { useTranslation } from 'react-i18next'
import useDocumentTitle from '@/hooks/use-document-title'
export type IExploreProps = {
children: React.ReactNode
@ -16,15 +17,16 @@ export type IExploreProps = {
const Explore: FC<IExploreProps> = ({
children,
}) => {
const { t } = useTranslation()
const router = useRouter()
const [controlUpdateInstalledApps, setControlUpdateInstalledApps] = useState(0)
const { userProfile, isCurrentWorkspaceDatasetOperator } = useAppContext()
const [hasEditPermission, setHasEditPermission] = useState(false)
const [installedApps, setInstalledApps] = useState<InstalledApp[]>([])
const { t } = useTranslation()
useDocumentTitle(t('common.menus.explore'))
useEffect(() => {
document.title = `${t('explore.title')} - Dify`;
(async () => {
const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {} })
if (!accounts)

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 bg-background-default py-2 pl-0 pr-2 sm:p-2'>
{installedApp.app.mode !== 'completion' && installedApp.app.mode !== 'workflow' && (
<ChatWithHistory installedAppInfo={installedApp} className='overflow-hidden rounded-2xl shadow-md' />
)}
{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

@ -2,7 +2,6 @@
import { useTranslation } from 'react-i18next'
import { Fragment, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useContextSelector } from 'use-context-selector'
import {
RiAccountCircleLine,
RiArrowRightUpLine,
@ -28,12 +27,12 @@ import { useGetDocLanguage } from '@/context/i18n'
import Avatar from '@/app/components/base/avatar'
import ThemeSwitcher from '@/app/components/base/theme-switcher'
import { logout } from '@/service/common'
import AppContext, { useAppContext } from '@/context/app-context'
import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context'
import { LicenseStatus } from '@/types/feature'
import { IS_CLOUD_EDITION } from '@/config'
import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
export default function AppSelector() {
const itemClassName = `
@ -42,7 +41,7 @@ export default function AppSelector() {
`
const router = useRouter()
const [aboutVisible, setAboutVisible] = useState(false)
const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures)
const { systemFeatures } = useGlobalPublicStore()
const { t } = useTranslation()
const { userProfile, langeniusVersionInfo, isCurrentWorkspaceOwner } = useAppContext()
@ -127,73 +126,75 @@ export default function AppSelector() {
</div>
</MenuItem>
</div>
<div className='p-1'>
<MenuItem>
<Link
className={cn(itemClassName, 'group justify-between',
'data-[active]:bg-state-base-hover',
)}
href={`https://docs.dify.ai/${docLanguage}/introduction`}
target='_blank' rel='noopener noreferrer'>
<RiBookOpenLine className='size-4 shrink-0 text-text-tertiary' />
<div className='system-md-regular grow px-1 text-text-secondary'>{t('common.userProfile.helpCenter')}</div>
<RiArrowRightUpLine className='size-[14px] shrink-0 text-text-tertiary' />
</Link>
</MenuItem>
<Support />
{IS_CLOUD_EDITION && isCurrentWorkspaceOwner && <Compliance />}
</div>
<div className='p-1'>
<MenuItem>
<Link
className={cn(itemClassName, 'group justify-between',
'data-[active]:bg-state-base-hover',
)}
href='https://roadmap.dify.ai'
target='_blank' rel='noopener noreferrer'>
<RiMap2Line className='size-4 shrink-0 text-text-tertiary' />
<div className='system-md-regular grow px-1 text-text-secondary'>{t('common.userProfile.roadmap')}</div>
<RiArrowRightUpLine className='size-[14px] shrink-0 text-text-tertiary' />
</Link>
</MenuItem>
{systemFeatures.license.status === LicenseStatus.NONE && <MenuItem>
<Link
className={cn(itemClassName, 'group justify-between',
'data-[active]:bg-state-base-hover',
)}
href='https://github.com/langgenius/dify'
target='_blank' rel='noopener noreferrer'>
<RiGithubLine className='size-4 shrink-0 text-text-tertiary' />
<div className='system-md-regular grow px-1 text-text-secondary'>{t('common.userProfile.github')}</div>
<div className='flex items-center gap-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-[5px] py-[3px]'>
<RiStarLine className='size-3 shrink-0 text-text-tertiary' />
<GithubStar className='system-2xs-medium-uppercase text-text-tertiary' />
</div>
</Link>
</MenuItem>}
{
document?.body?.getAttribute('data-public-site-about') !== 'hide' && (
<MenuItem>
<div className={cn(itemClassName, 'justify-between',
{!systemFeatures.branding.enabled && <>
<div className='p-1'>
<MenuItem>
<Link
className={cn(itemClassName, 'group justify-between',
'data-[active]:bg-state-base-hover',
)} onClick={() => setAboutVisible(true)}>
<RiInformation2Line className='size-4 shrink-0 text-text-tertiary' />
<div className='system-md-regular grow px-1 text-text-secondary'>{t('common.userProfile.about')}</div>
<div className='flex shrink-0 items-center'>
<div className='system-xs-regular mr-2 text-text-tertiary'>{langeniusVersionInfo.current_version}</div>
<Indicator color={langeniusVersionInfo.current_version === langeniusVersionInfo.latest_version ? 'green' : 'orange'} />
</div>
)}
href={`https://docs.dify.ai/${docLanguage}/introduction`}
target='_blank' rel='noopener noreferrer'>
<RiBookOpenLine className='size-4 shrink-0 text-text-tertiary' />
<div className='system-md-regular grow px-1 text-text-secondary'>{t('common.userProfile.helpCenter')}</div>
<RiArrowRightUpLine className='size-[14px] shrink-0 text-text-tertiary' />
</Link>
</MenuItem>
<Support />
{IS_CLOUD_EDITION && isCurrentWorkspaceOwner && <Compliance />}
</div>
<div className='p-1'>
<MenuItem>
<Link
className={cn(itemClassName, 'group justify-between',
'data-[active]:bg-state-base-hover',
)}
href='https://roadmap.dify.ai'
target='_blank' rel='noopener noreferrer'>
<RiMap2Line className='size-4 shrink-0 text-text-tertiary' />
<div className='system-md-regular grow px-1 text-text-secondary'>{t('common.userProfile.roadmap')}</div>
<RiArrowRightUpLine className='size-[14px] shrink-0 text-text-tertiary' />
</Link>
</MenuItem>
<MenuItem>
<Link
className={cn(itemClassName, 'group justify-between',
'data-[active]:bg-state-base-hover',
)}
href='https://github.com/langgenius/dify'
target='_blank' rel='noopener noreferrer'>
<RiGithubLine className='size-4 shrink-0 text-text-tertiary' />
<div className='system-md-regular grow px-1 text-text-secondary'>{t('common.userProfile.github')}</div>
<div className='flex items-center gap-0.5 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-[5px] py-[3px]'>
<RiStarLine className='size-3 shrink-0 text-text-tertiary' />
<GithubStar className='system-2xs-medium-uppercase text-text-tertiary' />
</div>
</MenuItem>
)
}
</div>
</Link>
</MenuItem>
{
document?.body?.getAttribute('data-public-site-about') !== 'hide' && (
<MenuItem>
<div className={cn(itemClassName, 'justify-between',
'data-[active]:bg-state-base-hover',
)} onClick={() => setAboutVisible(true)}>
<RiInformation2Line className='size-4 shrink-0 text-text-tertiary' />
<div className='system-md-regular grow px-1 text-text-secondary'>{t('common.userProfile.about')}</div>
<div className='flex shrink-0 items-center'>
<div className='system-xs-regular mr-2 text-text-tertiary'>{langeniusVersionInfo.current_version}</div>
<Indicator color={langeniusVersionInfo.current_version === langeniusVersionInfo.latest_version ? 'green' : 'orange'} />
</div>
</div>
</MenuItem>
)
}
</div>
</>}
<MenuItem disabled>
<div className='p-1'>
<div className={cn(itemClassName, 'hover:bg-transparent')}>
<RiTShirt2Line className='size-4 shrink-0 text-text-tertiary' />
<div className='system-md-regular grow px-1 text-text-secondary'>{t('common.theme.theme')}</div>
<ThemeSwitcher/>
<ThemeSwitcher />
</div>
</div>
</MenuItem>

View File

@ -25,6 +25,7 @@ import { LanguagesSupported } from '@/i18n/language'
import cn from '@/utils/classnames'
import Tooltip from '@/app/components/base/tooltip'
import { RiPencilLine } from '@remixicon/react'
import { useGlobalPublicStore } from '@/context/global-public-context'
dayjs.extend(relativeTime)
const MembersPage = () => {
@ -38,7 +39,7 @@ const MembersPage = () => {
}
const { locale } = useContext(I18n)
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager, systemFeatures } = useAppContext()
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext()
const { data, mutate } = useSWR(
{
url: '/workspaces/current/members',
@ -46,6 +47,7 @@ const MembersPage = () => {
},
fetchMembers,
)
const { systemFeatures } = useGlobalPublicStore()
const [inviteModalVisible, setInviteModalVisible] = useState(false)
const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([])
const [invitedModalVisible, setInvitedModalVisible] = useState(false)

View File

@ -1,5 +1,5 @@
'use client'
import { useCallback, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useContext } from 'use-context-selector'
import { RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
@ -18,6 +18,7 @@ import I18n from '@/context/i18n'
import 'react-multi-email/dist/style.css'
import { noop } from 'lodash-es'
import { useProviderContextSelector } from '@/context/provider-context'
type IInviteModalProps = {
isEmailSetup: boolean
onCancel: () => void
@ -30,13 +31,27 @@ const InviteModal = ({
onSend,
}: IInviteModalProps) => {
const { t } = useTranslation()
const licenseLimit = useProviderContextSelector(s => s.licenseLimit)
const refreshLicenseLimit = useProviderContextSelector(s => s.refreshLicenseLimit)
const [emails, setEmails] = useState<string[]>([])
const { notify } = useContext(ToastContext)
const [isLimited, setIsLimited] = useState(false)
const [isLimitExceeded, setIsLimitExceeded] = useState(false)
const [usedSize, setUsedSize] = useState(licenseLimit.workspace_members.size ?? 0)
useEffect(() => {
const limited = licenseLimit.workspace_members.limit > 0
const used = emails.length + licenseLimit.workspace_members.size
setIsLimited(limited)
setUsedSize(used)
setIsLimitExceeded(limited && (used > licenseLimit.workspace_members.limit))
}, [licenseLimit, emails])
const { locale } = useContext(I18n)
const [role, setRole] = useState<string>('normal')
const handleSend = useCallback(async () => {
if (isLimitExceeded)
return
if (emails.map((email: string) => emailRegex.test(email)).every(Boolean)) {
try {
const { result, invitation_results } = await inviteMember({
@ -45,6 +60,7 @@ const InviteModal = ({
})
if (result === 'success') {
refreshLicenseLimit()
onCancel()
onSend(invitation_results)
}
@ -54,7 +70,7 @@ const InviteModal = ({
else {
notify({ type: 'error', message: t('common.members.emailInvalid') })
}
}, [role, emails, notify, onCancel, onSend, t])
}, [isLimitExceeded, emails, role, locale, onCancel, onSend, notify, t])
return (
<div className={cn(s.wrap)}>
@ -82,7 +98,7 @@ const InviteModal = ({
<div>
<div className='mb-2 text-sm font-medium text-text-primary'>{t('common.members.email')}</div>
<div className='mb-8 flex h-36 items-stretch'>
<div className='mb-8 flex h-36 flex-col items-stretch'>
<ReactMultiEmail
className={cn('w-full border-components-input-border-active !bg-components-input-bg-normal px-3 pt-2 outline-none',
'appearance-none overflow-y-auto rounded-lg text-sm !text-text-primary',
@ -101,6 +117,14 @@ const InviteModal = ({
}
placeholder={t('common.members.emailPlaceholder') || ''}
/>
<div className={
cn('system-xs-regular flex items-center justify-end text-text-tertiary',
(isLimited && usedSize > licenseLimit.workspace_members.limit) ? 'text-text-destructive' : '')}
>
<span>{usedSize}</span>
<span>/</span>
<span>{isLimited ? licenseLimit.workspace_members.limit : t('common.license.unlimited')}</span>
</div>
</div>
<div className='mb-6'>
<RoleSelector value={role} onChange={setRole} />
@ -109,7 +133,7 @@ const InviteModal = ({
tabIndex={0}
className='w-full'
onClick={handleSend}
disabled={!emails.length}
disabled={!emails.length || isLimitExceeded}
variant='primary'
>
{t('common.members.sendInvite')}

View File

@ -23,7 +23,7 @@ import {
import InstallFromMarketplace from './install-from-marketplace'
import { useProviderContext } from '@/context/provider-context'
import cn from '@/utils/classnames'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
type Props = {
searchText: string
@ -40,7 +40,7 @@ const ModelProviderPage = ({ searchText }: Props) => {
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
const { data: ttsDefaultModel } = useDefaultModel(ModelTypeEnum.tts)
const { modelProviders: providers } = useProviderContext()
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const defaultModelNotConfigured = !textGenerationDefaultModel && !embeddingsDefaultModel && !speech2textDefaultModel && !rerankDefaultModel && !ttsDefaultModel
const [configuredProviders, notConfiguredProviders] = useMemo(() => {
const configuredProviders: ModelProvider[] = []

View File

@ -1,16 +1,15 @@
'use client'
import AppContext from '@/context/app-context'
import { LicenseStatus } from '@/types/feature'
import { useTranslation } from 'react-i18next'
import { useContextSelector } from 'use-context-selector'
import dayjs from 'dayjs'
import PremiumBadge from '../../base/premium-badge'
import { RiHourglass2Fill } from '@remixicon/react'
import { useGlobalPublicStore } from '@/context/global-public-context'
const LicenseNav = () => {
const { t } = useTranslation()
const systemFeatures = useContextSelector(AppContext, s => s.systemFeatures)
const { systemFeatures } = useGlobalPublicStore()
if (systemFeatures.license?.status === LicenseStatus.EXPIRING) {
const expiredAt = systemFeatures.license?.expired_at

View File

@ -10,11 +10,11 @@ import {
createContext,
useContextSelector,
} from 'use-context-selector'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import type { FilterState } from './filter-management'
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import { noop } from 'lodash-es'
import { PLUGIN_PAGE_TABS_MAP, usePluginPageTabs } from '../hooks'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type PluginPageContextValue = {
containerRef: React.RefObject<HTMLDivElement>
@ -61,7 +61,7 @@ export const PluginPageContextProvider = ({
})
const [currentPluginID, setCurrentPluginID] = useState<string | undefined>()
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const tabs = usePluginPageTabs()
const options = useMemo(() => {
return enable_marketplace ? tabs : tabs.filter(tab => tab.value !== PLUGIN_PAGE_TABS_MAP.marketplace)

View File

@ -6,19 +6,19 @@ import InstallFromGitHub from '@/app/components/plugins/install-plugin/install-f
import InstallFromLocalPackage from '@/app/components/plugins/install-plugin/install-from-local-package'
import { usePluginPageContext } from '../context'
import { Group } from '@/app/components/base/icons/src/vender/other'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import Line from '../../marketplace/empty/line'
import { useInstalledPluginList } from '@/service/use-plugins'
import { useTranslation } from 'react-i18next'
import { SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config'
import { noop } from 'lodash-es'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Empty = () => {
const { t } = useTranslation()
const fileInputRef = useRef<HTMLInputElement>(null)
const [selectedAction, setSelectedAction] = useState<string | null>(null)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const setActiveTab = usePluginPageContext(v => v.setActiveTab)
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {

View File

@ -25,7 +25,6 @@ import TabSlider from '@/app/components/base/tab-slider'
import Tooltip from '@/app/components/base/tooltip'
import cn from '@/utils/classnames'
import PermissionSetModal from '@/app/components/plugins/permission-setting-modal/modal'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import InstallFromMarketplace from '../install-plugin/install-from-marketplace'
import {
useRouter,
@ -42,6 +41,8 @@ import I18n from '@/context/i18n'
import { noop } from 'lodash-es'
import { PLUGIN_TYPE_SEARCH_MAP } from '../marketplace/plugin-type-switch'
import { PLUGIN_PAGE_TABS_MAP } from '../hooks'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
const PACKAGE_IDS_KEY = 'package-ids'
const BUNDLE_INFO_KEY = 'bundle-info'
@ -58,8 +59,7 @@ const PluginPage = ({
const { locale } = useContext(I18n)
const searchParams = useSearchParams()
const { replace } = useRouter()
document.title = `${t('plugin.metadata.title')} - Dify`
useDocumentTitle(t('plugin.metadata.title'))
// just support install one package now
const packageId = useMemo(() => {
@ -136,7 +136,7 @@ const PluginPage = ({
const options = usePluginPageContext(v => v.options)
const activeTab = usePluginPageContext(v => v.activeTab)
const setActiveTab = usePluginPageContext(v => v.setActiveTab)
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const isPluginsTab = useMemo(() => activeTab === PLUGIN_PAGE_TABS_MAP.plugins, [activeTab])
const isExploringMarketplace = useMemo(() => {

View File

@ -14,10 +14,10 @@ import {
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { useTranslation } from 'react-i18next'
import { SUPPORT_INSTALL_LOCAL_FILE_EXTENSIONS } from '@/config'
import { noop } from 'lodash-es'
import { useGlobalPublicStore } from '@/context/global-public-context'
type Props = {
onSwitchToMarketplaceTab: () => void
@ -30,7 +30,7 @@ const InstallPluginDropdown = ({
const [isMenuOpen, setIsMenuOpen] = useState(false)
const [selectedAction, setSelectedAction] = useState<string | null>(null)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]

View File

@ -3,8 +3,8 @@ import { useAppContext } from '@/context/app-context'
import Toast from '../../base/toast'
import { useTranslation } from 'react-i18next'
import { useInvalidatePermissions, useMutationPermissions, usePermissions } from '@/service/use-plugins'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { useMemo } from 'react'
import { useGlobalPublicStore } from '@/context/global-public-context'
const hasPermission = (permission: PermissionType | undefined, isAdmin: boolean) => {
if (!permission)
@ -46,7 +46,7 @@ const usePermission = () => {
}
export const useCanInstallPluginFromMarketplace = () => {
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const { canManagement } = usePermission()
const canInstallPluginFromMarketplace = useMemo(() => {

View File

@ -13,6 +13,7 @@ import { checkOrSetAccessToken } from '../utils'
import MenuDropdown from './menu-dropdown'
import RunBatch from './run-batch'
import ResDownload from './run-batch/res-download'
import AppUnavailable from '../../base/app-unavailable'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import RunOnce from '@/app/components/share/text-generation/run-once'
import { fetchSavedMessage as doFetchSavedMessage, fetchAppInfo, fetchAppParams, removeMessage, saveMessage } from '@/service/share'
@ -38,6 +39,10 @@ import { Resolution, TransferMethod } from '@/types/app'
import { useAppFavicon } from '@/hooks/use-app-favicon'
import DifyLogo from '@/app/components/base/logo/dify-logo'
import cn from '@/utils/classnames'
import { useGetAppAccessMode, useGetUserCanAccessApp } from '@/service/access-control'
import { AccessMode } from '@/models/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group.
enum TaskStatus {
@ -98,14 +103,25 @@ const TextGeneration: FC<IMainProps> = ({
doSetInputs(newInputs)
inputsRef.current = newInputs
}, [])
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const [appId, setAppId] = useState<string>('')
const [siteInfo, setSiteInfo] = useState<SiteInfo | null>(null)
const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false)
const [customConfig, setCustomConfig] = useState<Record<string, any> | null>(null)
const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null)
const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | null>(null)
const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null)
const { isPending: isGettingAccessMode, data: appAccessMode } = useGetAppAccessMode({
appId,
isInstalledApp,
enabled: systemFeatures.webapp_auth.enabled,
})
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({
appId,
isInstalledApp,
enabled: systemFeatures.webapp_auth.enabled,
})
// save message
const [savedMessages, setSavedMessages] = useState<SavedMessage[]>([])
const fetchSavedMessage = async () => {
@ -395,10 +411,9 @@ const TextGeneration: FC<IMainProps> = ({
useEffect(() => {
(async () => {
const [appData, appParams]: any = await fetchInitData()
const { app_id: appId, site: siteInfo, can_replace_logo, custom_config } = appData
const { app_id: appId, site: siteInfo, custom_config } = appData
setAppId(appId)
setSiteInfo(siteInfo as SiteInfo)
setCanReplaceLogo(can_replace_logo)
setCustomConfig(custom_config)
changeLanguage(siteInfo.default_language)
@ -422,14 +437,7 @@ const TextGeneration: FC<IMainProps> = ({
}, [])
// Can Use metadata(https://beta.nextjs.org/docs/api-reference/metadata) to set title. But it only works in server side client.
useEffect(() => {
if (siteInfo?.title) {
if (canReplaceLogo)
document.title = `${siteInfo.title}`
else
document.title = `${siteInfo.title} - Powered by Dify`
}
}, [siteInfo?.title, canReplaceLogo])
useDocumentTitle(siteInfo?.title || t('share.generation.title'))
useAppFavicon({
enable: !isInstalledApp,
@ -528,12 +536,14 @@ const TextGeneration: FC<IMainProps> = ({
</div>
)
if (!appId || !siteInfo || !promptConfig) {
if (!appId || !siteInfo || !promptConfig || (systemFeatures.webapp_auth.enabled && (isGettingAccessMode || isCheckingPermission))) {
return (
<div className='flex h-screen items-center'>
<Loading type='app' />
</div>)
}
if (systemFeatures.webapp_auth.enabled && !userCanAccessResult?.result)
return <AppUnavailable code={403} unknownReason='no permission.' />
return (
<div className={cn(
@ -559,7 +569,7 @@ const TextGeneration: FC<IMainProps> = ({
imageUrl={siteInfo.icon_url}
/>
<div className='system-md-semibold grow truncate text-text-secondary'>{siteInfo.title}</div>
<MenuDropdown data={siteInfo} />
<MenuDropdown hideLogout={isInstalledApp || appAccessMode?.accessMode === AccessMode.PUBLIC} data={siteInfo} />
</div>
{siteInfo.description && (
<div className='system-xs-regular text-text-tertiary'>{siteInfo.description}</div>
@ -631,10 +641,9 @@ const TextGeneration: FC<IMainProps> = ({
!isPC && resultExisted && 'rounded-b-2xl border-b-[0.5px] border-divider-regular',
)}>
<div className='system-2xs-medium-uppercase text-text-tertiary'>{t('share.chat.poweredBy')}</div>
{customConfig?.replace_webapp_logo && (
<img src={customConfig?.replace_webapp_logo} alt='logo' className='block h-5 w-auto' />
)}
{!customConfig?.replace_webapp_logo && (
{systemFeatures.branding.enabled ? (
<img src={systemFeatures.branding.login_page_logo} alt='logo' className='block h-5 w-auto' />
) : (
<DifyLogo size='small' />
)}
</div>

View File

@ -1,9 +1,9 @@
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'
import cn from 'classnames'
type Props = {
data?: SiteInfo

View File

@ -6,27 +6,32 @@ 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 Divider from '@/app/components/base/divider'
import ThemeSwitcher from '@/app/components/base/theme-switcher'
import InfoModal from './info-modal'
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)
@ -39,6 +44,11 @@ const MenuDropdown: FC<Props> = ({
setOpen(!openRef.current)
}, [setOpen])
const handleLogout = useCallback(() => {
removeAccessToken()
router.replace(`/webapp-signin?redirect_url=${window.location.href}`)
}, [router])
const [show, setShow] = useState(false)
return (
@ -64,7 +74,7 @@ const MenuDropdown: FC<Props> = ({
<div className='p-1'>
<div className={cn('system-md-regular flex cursor-pointer items-center rounded-lg py-1.5 pl-3 pr-2 text-text-secondary')}>
<div className='grow'>{t('common.theme.theme')}</div>
<ThemeSwitcher/>
<ThemeSwitcher />
</div>
</div>
<Divider type='horizontal' className='my-0' />

View File

@ -15,14 +15,14 @@ import WorkflowToolEmpty from '@/app/components/tools/add-tool-modal/empty'
import Card from '@/app/components/plugins/card'
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { useAllToolProviders } from '@/service/use-tools'
import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
import { useGlobalPublicStore } from '@/context/global-public-context'
const ProviderList = () => {
const { t } = useTranslation()
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const containerRef = useRef<HTMLDivElement>(null)
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures)
const [activeTab, setActiveTab] = useTabSearchParams({
defaultTab: 'builtin',
@ -144,8 +144,8 @@ const ProviderList = () => {
/>
)
}
</div>
</div>
</div >
</div >
{currentProvider && !currentProvider.plugin_id && (
<ProviderDetail
collection={currentProvider}

View File

@ -26,9 +26,8 @@ import type { StartNodeType } from '@/app/components/workflow/nodes/start/types'
import { useToastContext } from '@/app/components/base/toast'
import { usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow'
import type { PublishWorkflowParams } from '@/types/workflow'
import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
import { fetchAppDetail } from '@/service/apps'
import { useStore as useAppStore } from '@/app/components/app/store'
import { useSelector as useAppSelector } from '@/context/app-context'
const FeaturesTrigger = () => {
const { t } = useTranslation()
@ -36,7 +35,6 @@ const FeaturesTrigger = () => {
const appDetail = useAppStore(s => s.appDetail)
const appID = appDetail?.id
const setAppDetail = useAppStore(s => s.setAppDetail)
const systemFeatures = useAppSelector(state => state.systemFeatures)
const {
nodesReadOnly,
getNodesReadOnly,
@ -85,18 +83,12 @@ const FeaturesTrigger = () => {
const updateAppDetail = useCallback(async () => {
try {
const res = await fetchAppDetail({ url: '/apps', id: appID! })
if (systemFeatures.enable_web_sso_switch_component) {
const ssoRes = await fetchAppSSO({ appId: appID! })
setAppDetail({ ...res, enable_sso: ssoRes.enabled })
}
else {
setAppDetail({ ...res })
}
setAppDetail({ ...res })
}
catch (error) {
console.error(error)
}
}, [appID, setAppDetail, systemFeatures.enable_web_sso_switch_component])
}, [appID, setAppDetail])
const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!)
const onPublish = useCallback(async (params?: PublishWorkflowParams) => {
if (await handleCheckBeforePublish()) {

View File

@ -21,7 +21,7 @@ import ActionButton from '../../base/action-button'
import { RiAddLine } from '@remixicon/react'
import { PluginType } from '../../plugins/types'
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
import { useSelector as useAppContextSelector } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
type AllToolsProps = {
className?: string
@ -87,7 +87,7 @@ const AllTools = ({
plugins: notInstalledPlugins = [],
} = useMarketplacePlugins()
const { enable_marketplace } = useAppContextSelector(s => s.systemFeatures)
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
useEffect(() => {
if (enable_marketplace) return

View File

@ -112,7 +112,7 @@ const IterationResultPanel: FC<Props> = ({
'transition-all duration-200',
expandedIterations[index]
? 'opacity-100'
: 'max-h-0 opacity-0 overflow-hidden',
: 'max-h-0 overflow-hidden opacity-0',
)}>
<TracingPanel
list={iteration}

View File

@ -118,7 +118,7 @@ const LoopResultPanel: FC<Props> = ({
'transition-all duration-200',
expandedLoops[index]
? 'opacity-100'
: 'max-h-0 opacity-0 overflow-hidden',
: 'max-h-0 overflow-hidden opacity-0',
)}>
{
loopVariableMap?.[index] && (

View File

@ -85,7 +85,7 @@ const LoopResultPanel: FC<Props> = ({
'transition-all duration-200',
expandedLoops[index]
? 'opacity-100'
: 'max-h-0 opacity-0 overflow-hidden',
: 'max-h-0 overflow-hidden opacity-0',
)}>
<TracingPanel
list={loop}

View File

@ -5,19 +5,23 @@ import { useSearchParams } from 'next/navigation'
import Header from '../signin/_header'
import ForgotPasswordForm from './ForgotPasswordForm'
import ChangePasswordForm from '@/app/forgot-password/ChangePasswordForm'
import useDocumentTitle from '@/hooks/use-document-title'
import { useGlobalPublicStore } from '@/context/global-public-context'
const ForgotPassword = () => {
useDocumentTitle('')
const searchParams = useSearchParams()
const token = searchParams.get('token')
const { systemFeatures } = useGlobalPublicStore()
return (
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
<Header />
{token ? <ChangePasswordForm /> : <ForgotPasswordForm />}
<div className='px-8 py-6 text-sm font-normal text-text-tertiary'>
{!systemFeatures.branding.enabled && <div className='px-8 py-6 text-sm font-normal text-text-tertiary'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div>
</div>}
</div>
</div>
)

View File

@ -8,8 +8,10 @@ import Button from '@/app/components/base/button'
import { basePath } from '@/utils/var'
import { fetchInitValidateStatus, initValidate } from '@/service/common'
import type { InitValidateStatusResponse } from '@/models/common'
import useDocumentTitle from '@/hooks/use-document-title'
const InitPasswordPopup = () => {
useDocumentTitle('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(true)
const [validated, setValidated] = useState(false)

View File

@ -16,6 +16,7 @@ import Button from '@/app/components/base/button'
import { fetchInitValidateStatus, fetchSetupStatus, setup } from '@/service/common'
import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common'
import useDocumentTitle from '@/hooks/use-document-title'
const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
@ -33,6 +34,7 @@ const accountFormSchema = z.object({
type AccountFormValues = z.infer<typeof accountFormSchema>
const InstallForm = () => {
useDocumentTitle('')
const { t } = useTranslation()
const router = useRouter()
const [showPassword, setShowPassword] = React.useState(false)

View File

@ -1,17 +1,20 @@
'use client'
import React from 'react'
import Header from '../signin/_header'
import InstallForm from './installForm'
import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Install = () => {
const { systemFeatures } = useGlobalPublicStore()
return (
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
<Header />
<InstallForm />
<div className='px-8 py-6 text-sm font-normal text-text-tertiary'>
{!systemFeatures.branding.enabled && <div className='px-8 py-6 text-sm font-normal text-text-tertiary'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div>
</div>}
</div>
</div>
)

View File

@ -1,5 +1,5 @@
import type { Viewport } from 'next'
import RoutePrefixHandle from './routePrefixHandle'
import type { Viewport } from 'next'
import I18nServer from './components/i18n-server'
import BrowserInitor from './components/browser-initor'
import SentryInitor from './components/sentry-initor'
@ -8,10 +8,7 @@ import { TanstackQueryIniter } from '@/context/query-client'
import { ThemeProvider } from 'next-themes'
import './styles/globals.css'
import './styles/markdown.scss'
export const metadata = {
title: 'Dify',
}
import GlobalPublicStoreProvider from '@/context/global-public-context'
export const viewport: Viewport = {
width: 'device-width',
@ -68,7 +65,9 @@ const LocaleLayout = async ({
disableTransitionOnChange
>
<I18nServer>
{children}
<GlobalPublicStoreProvider>
{children}
</GlobalPublicStoreProvider>
</I18nServer>
</ThemeProvider>
</TanstackQueryIniter>

View File

@ -1,8 +1,11 @@
'use client'
import Header from '../signin/_header'
import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
export default async function SignInLayout({ children }: any) {
export default function SignInLayout({ children }: any) {
const { systemFeatures } = useGlobalPublicStore()
return <>
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
@ -18,9 +21,9 @@ export default async function SignInLayout({ children }: any) {
{children}
</div>
</div>
<div className='system-xs-regular px-8 py-6 text-text-tertiary'>
{!systemFeatures.branding.enabled && <div className='system-xs-regular px-8 py-6 text-text-tertiary'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div>
</div>}
</div>
</div>
</>

View File

@ -13,9 +13,11 @@ import Toast from '@/app/components/base/toast'
import { sendResetPasswordCode } from '@/service/common'
import I18NContext from '@/context/i18n'
import { noop } from 'lodash-es'
import useDocumentTitle from '@/hooks/use-document-title'
export default function CheckCode() {
const { t } = useTranslation()
useDocumentTitle('')
const searchParams = useSearchParams()
const router = useRouter()
const [email, setEmail] = useState('')

View File

@ -0,0 +1,34 @@
'use client'
import type { FC } from 'react'
import classNames from '@/utils/classnames'
import { useSelector } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
type LoginLogoProps = {
className?: string
}
const LoginLogo: FC<LoginLogoProps> = ({
className,
}) => {
const { systemFeatures } = useGlobalPublicStore()
const { theme } = useSelector((s) => {
return {
theme: s.theme,
}
})
let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png`
if (systemFeatures.branding.enabled)
src = systemFeatures.branding.login_page_logo
return (
<img
src={src}
className={classNames('block w-auto h-10', className)}
alt='logo'
/>
)
}
export default LoginLogo

View File

@ -1,8 +1,13 @@
'use client'
import Header from './_header'
import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
export default async function SignInLayout({ children }: any) {
export default function SignInLayout({ children }: any) {
const { systemFeatures } = useGlobalPublicStore()
useDocumentTitle('')
return <>
<div className={cn('flex min-h-screen w-full justify-center bg-background-default-burn p-6')}>
<div className={cn('flex w-full shrink-0 flex-col rounded-2xl border border-effects-highlight bg-background-default-subtle')}>
@ -12,9 +17,9 @@ export default async function SignInLayout({ children }: any) {
{children}
</div>
</div>
<div className='system-xs-regular px-8 py-6 text-text-tertiary'>
{systemFeatures.branding.enabled === false && <div className='system-xs-regular px-8 py-6 text-text-tertiary'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div>
</div>}
</div>
</div>
</>

View File

@ -9,10 +9,11 @@ import MailAndPasswordAuth from './components/mail-and-password-auth'
import SocialAuth from './components/social-auth'
import SSOAuth from './components/sso-auth'
import cn from '@/utils/classnames'
import { getSystemFeatures, invitationCheck } from '@/service/common'
import { LicenseStatus, defaultSystemFeatures } from '@/types/feature'
import { invitationCheck } from '@/service/common'
import { LicenseStatus } from '@/types/feature'
import Toast from '@/app/components/base/toast'
import { IS_CE_EDITION } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
const NormalForm = () => {
const { t } = useTranslation()
@ -23,7 +24,7 @@ const NormalForm = () => {
const message = decodeURIComponent(searchParams.get('message') || '')
const invite_token = decodeURIComponent(searchParams.get('invite_token') || '')
const [isLoading, setIsLoading] = useState(true)
const [systemFeatures, setSystemFeatures] = useState(defaultSystemFeatures)
const { systemFeatures } = useGlobalPublicStore()
const [authType, updateAuthType] = useState<'code' | 'password'>('password')
const [showORLine, setShowORLine] = useState(false)
const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false)
@ -46,12 +47,9 @@ const NormalForm = () => {
message,
})
}
const features = await getSystemFeatures()
const allFeatures = { ...defaultSystemFeatures, ...features }
setSystemFeatures(allFeatures)
setAllMethodsAreDisabled(!allFeatures.enable_social_oauth_login && !allFeatures.enable_email_code_login && !allFeatures.enable_email_password_login && !allFeatures.sso_enforced_for_signin)
setShowORLine((allFeatures.enable_social_oauth_login || allFeatures.sso_enforced_for_signin) && (allFeatures.enable_email_code_login || allFeatures.enable_email_password_login))
updateAuthType(allFeatures.enable_email_password_login ? 'password' : 'code')
setAllMethodsAreDisabled(!systemFeatures.enable_social_oauth_login && !systemFeatures.enable_email_code_login && !systemFeatures.enable_email_password_login && !systemFeatures.sso_enforced_for_signin)
setShowORLine((systemFeatures.enable_social_oauth_login || systemFeatures.sso_enforced_for_signin) && (systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login))
updateAuthType(systemFeatures.enable_email_password_login ? 'password' : 'code')
if (isInviteLink) {
const checkRes = await invitationCheck({
url: '/activate/check',
@ -65,10 +63,9 @@ const NormalForm = () => {
catch (error) {
console.error(error)
setAllMethodsAreDisabled(true)
setSystemFeatures(defaultSystemFeatures)
}
finally { setIsLoading(false) }
}, [consoleToken, refreshToken, message, router, invite_token, isInviteLink])
}, [consoleToken, refreshToken, message, router, invite_token, isInviteLink, systemFeatures])
useEffect(() => {
init()
}, [init])
@ -132,11 +129,11 @@ const NormalForm = () => {
{isInviteLink
? <div className="mx-auto w-full">
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.join')}{workspaceName}</h2>
<p className='body-md-regular mt-2 text-text-tertiary'>{t('login.joinTipStart')}{workspaceName}{t('login.joinTipEnd')}</p>
{!systemFeatures.branding.enabled && <p className='body-md-regular mt-2 text-text-tertiary'>{t('login.joinTipStart')}{workspaceName}{t('login.joinTipEnd')}</p>}
</div>
: <div className="mx-auto w-full">
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.pageTitle')}</h2>
<p className='body-md-regular mt-2 text-text-tertiary'>{t('login.welcome')}</p>
{!systemFeatures.branding.enabled && <p className='body-md-regular mt-2 text-text-tertiary'>{t('login.welcome')}</p>}
</div>}
<div className="relative">
<div className="mt-6 flex flex-col gap-3">
@ -184,29 +181,31 @@ const NormalForm = () => {
</div>
</div>
</>}
<div className="system-xs-regular mt-2 block w-full text-text-tertiary">
{t('login.tosDesc')}
&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
target='_blank' rel='noopener noreferrer'
href='https://dify.ai/terms'
>{t('login.tos')}</Link>
&nbsp;&&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
target='_blank' rel='noopener noreferrer'
href='https://dify.ai/privacy'
>{t('login.pp')}</Link>
</div>
{IS_CE_EDITION && <div className="w-hull system-xs-regular mt-2 block text-text-tertiary">
{t('login.goToInit')}
&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
href='/install'
>{t('login.setAdminAccount')}</Link>
</div>}
{!systemFeatures.branding.enabled && <>
<div className="system-xs-regular mt-2 block w-full text-text-tertiary">
{t('login.tosDesc')}
&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
target='_blank' rel='noopener noreferrer'
href='https://dify.ai/terms'
>{t('login.tos')}</Link>
&nbsp;&&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
target='_blank' rel='noopener noreferrer'
href='https://dify.ai/privacy'
>{t('login.pp')}</Link>
</div>
{IS_CE_EDITION && <div className="w-hull system-xs-regular mt-2 block text-text-tertiary">
{t('login.goToInit')}
&nbsp;
<Link
className='system-xs-medium text-text-secondary hover:underline'
href='/install'
>{t('login.setAdminAccount')}</Link>
</div>}
</>}
</div>
</div>

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

@ -6,17 +6,14 @@ import { createContext, useContext, useContextSelector } from 'use-context-selec
import type { FC, ReactNode } from 'react'
import { fetchAppList } from '@/service/apps'
import Loading from '@/app/components/base/loading'
import { fetchCurrentWorkspace, fetchLanggeniusVersion, fetchUserProfile, getSystemFeatures } from '@/service/common'
import { fetchCurrentWorkspace, fetchLanggeniusVersion, fetchUserProfile } from '@/service/common'
import type { App } from '@/types/app'
import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
import MaintenanceNotice from '@/app/components/header/maintenance-notice'
import type { SystemFeatures } from '@/types/feature'
import { defaultSystemFeatures } from '@/types/feature'
import { noop } from 'lodash-es'
export type AppContextValue = {
apps: App[]
systemFeatures: SystemFeatures
mutateApps: VoidFunction
userProfile: UserProfileResponse
mutateUserProfile: VoidFunction
@ -53,7 +50,6 @@ const initialWorkspaceInfo: ICurrentWorkspace = {
}
const AppContext = createContext<AppContextValue>({
systemFeatures: defaultSystemFeatures,
apps: [],
mutateApps: noop,
userProfile: {
@ -92,10 +88,6 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
const { data: userProfileResponse, mutate: mutateUserProfile } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile)
const { data: currentWorkspaceResponse, mutate: mutateCurrentWorkspace, isLoading: isLoadingCurrentWorkspace } = useSWR({ url: '/workspaces/current', params: {} }, fetchCurrentWorkspace)
const { data: systemFeatures } = useSWR({ url: '/console/system-features' }, getSystemFeatures, {
fallbackData: defaultSystemFeatures,
})
const [userProfile, setUserProfile] = useState<UserProfileResponse>()
const [langeniusVersionInfo, setLangeniusVersionInfo] = useState<LangGeniusVersionResponse>(initialLangeniusVersionInfo)
const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo)
@ -129,7 +121,6 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
return (
<AppContext.Provider value={{
apps: appList.data,
systemFeatures: { ...defaultSystemFeatures, ...systemFeatures },
mutateApps,
userProfile,
mutateUserProfile,

View File

@ -0,0 +1,46 @@
'use client'
import { create } from 'zustand'
import { useQuery } from '@tanstack/react-query'
import type { FC, PropsWithChildren } from 'react'
import { useEffect } from 'react'
import type { SystemFeatures } from '@/types/feature'
import { defaultSystemFeatures } from '@/types/feature'
import { getSystemFeatures } from '@/service/common'
import Loading from '@/app/components/base/loading'
type GlobalPublicStore = {
isPending: boolean
setIsPending: (isPending: boolean) => void
systemFeatures: SystemFeatures
setSystemFeatures: (systemFeatures: SystemFeatures) => void
}
export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({
isPending: true,
setIsPending: (isPending: boolean) => set(() => ({ isPending })),
systemFeatures: defaultSystemFeatures,
setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })),
}))
const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({
children,
}) => {
const { isPending, data } = useQuery({
queryKey: ['systemFeatures'],
queryFn: getSystemFeatures,
})
const { setSystemFeatures, setIsPending } = useGlobalPublicStore()
useEffect(() => {
if (data)
setSystemFeatures({ ...defaultSystemFeatures, ...data })
}, [data, setSystemFeatures])
useEffect(() => {
setIsPending(isPending)
}, [isPending, setIsPending])
if (isPending)
return <div className='flex h-screen w-screen items-center justify-center'><Loading /></div>
return <>{children}</>
}
export default GlobalPublicStoreProvider

View File

@ -48,6 +48,14 @@ type ProviderContextState = {
enableEducationPlan: boolean
isEducationWorkspace: boolean
isEducationAccount: boolean
webappCopyrightEnabled: boolean
licenseLimit: {
workspace_members: {
size: number
limit: number
}
},
refreshLicenseLimit: () => void
}
const ProviderContext = createContext<ProviderContextState>({
modelProviders: [],
@ -81,6 +89,14 @@ const ProviderContext = createContext<ProviderContextState>({
enableEducationPlan: false,
isEducationWorkspace: false,
isEducationAccount: false,
webappCopyrightEnabled: false,
licenseLimit: {
workspace_members: {
size: 0,
limit: 0,
},
},
refreshLicenseLimit: noop,
})
export const useProviderContext = () => useContext(ProviderContext)
@ -107,6 +123,13 @@ export const ProviderContextProvider = ({
const [enableReplaceWebAppLogo, setEnableReplaceWebAppLogo] = useState(false)
const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false)
const [datasetOperatorEnabled, setDatasetOperatorEnabled] = useState(false)
const [webappCopyrightEnabled, setWebappCopyrightEnabled] = useState(false)
const [licenseLimit, setLicenseLimit] = useState({
workspace_members: {
size: 0,
limit: 0,
},
})
const [enableEducationPlan, setEnableEducationPlan] = useState(false)
const [isEducationWorkspace, setIsEducationWorkspace] = useState(false)
@ -135,6 +158,14 @@ export const ProviderContextProvider = ({
setModelLoadBalancingEnabled(true)
if (data.dataset_operator_enabled)
setDatasetOperatorEnabled(true)
if (data.model_load_balancing_enabled)
setModelLoadBalancingEnabled(true)
if (data.dataset_operator_enabled)
setDatasetOperatorEnabled(true)
if (data.webapp_copyright_enabled)
setWebappCopyrightEnabled(true)
if (data.workspace_members)
setLicenseLimit({ workspace_members: data.workspace_members })
}
catch (error) {
console.error('Failed to fetch plan info:', error)
@ -192,6 +223,9 @@ export const ProviderContextProvider = ({
enableEducationPlan,
isEducationWorkspace,
isEducationAccount: isEducationAccount?.result || false,
webappCopyrightEnabled,
licenseLimit,
refreshLicenseLimit: fetchPlan,
}}>
{children}
</ProviderContext.Provider>

View File

@ -167,7 +167,7 @@ export default combine(
'sonarjs/max-lines': 'warn', // max 1000 lines
'sonarjs/no-variable-usage-before-declaration': 'error',
// security
// eslint-disable-next-line sonarjs/no-hardcoded-passwords
'sonarjs/no-hardcoded-passwords': 'off', // detect the wrong code that is not password.
'sonarjs/no-hardcoded-secrets': 'off',
'sonarjs/pseudo-random': 'off',

View File

@ -0,0 +1,65 @@
import { defaultSystemFeatures } from '@/types/feature'
import { act, renderHook } from '@testing-library/react'
import useDocumentTitle from './use-document-title'
import { useGlobalPublicStore } from '@/context/global-public-context'
jest.mock('@/service/common', () => ({
getSystemFeatures: jest.fn(() => ({ ...defaultSystemFeatures })),
}))
describe('title should be empty if systemFeatures is pending', () => {
act(() => {
useGlobalPublicStore.setState({
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } },
isPending: true,
})
})
it('document title should be empty if set title', () => {
renderHook(() => useDocumentTitle('test'))
expect(document.title).toBe('')
})
it('document title should be empty if not set title', () => {
renderHook(() => useDocumentTitle(''))
expect(document.title).toBe('')
})
})
describe('use default branding', () => {
beforeEach(() => {
act(() => {
useGlobalPublicStore.setState({
isPending: false,
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: false } },
})
})
})
it('document title should be test-Dify if set title', () => {
renderHook(() => useDocumentTitle('test'))
expect(document.title).toBe('test - Dify')
})
it('document title should be Dify if not set title', () => {
renderHook(() => useDocumentTitle(''))
expect(document.title).toBe('Dify')
})
})
describe('use specific branding', () => {
beforeEach(() => {
act(() => {
useGlobalPublicStore.setState({
isPending: false,
systemFeatures: { ...defaultSystemFeatures, branding: { ...defaultSystemFeatures.branding, enabled: true, application_title: 'Test' } },
})
})
})
it('document title should be test-Test if set title', () => {
renderHook(() => useDocumentTitle('test'))
expect(document.title).toBe('test - Test')
})
it('document title should be Test if not set title', () => {
renderHook(() => useDocumentTitle(''))
expect(document.title).toBe('Test')
})
})

View File

@ -0,0 +1,23 @@
'use client'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useFavicon, useTitle } from 'ahooks'
export default function useDocumentTitle(title: string) {
const isPending = useGlobalPublicStore(s => s.isPending)
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const prefix = title ? `${title} - ` : ''
let titleStr = ''
let favicon = ''
if (isPending === false) {
if (systemFeatures.branding.enabled) {
titleStr = `${prefix}${systemFeatures.branding.application_title}`
favicon = systemFeatures.branding.favicon
}
else {
titleStr = `${prefix}Dify`
favicon = '/favicon.ico'
}
}
useTitle(titleStr)
useFavicon(favicon)
}

View File

@ -1,4 +1,5 @@
import { usePathname, useSearchParams } from 'next/navigation'
'use client'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useState } from 'react'
type UseTabSearchParamsOptions = {
@ -25,7 +26,8 @@ export const useTabSearchParams = ({
disableSearchParams = false,
}: UseTabSearchParamsOptions) => {
const pathnameFromHook = usePathname()
const pathName = window?.location?.pathname || pathnameFromHook
const router = useRouter()
const pathName = pathnameFromHook || window?.location?.pathname
const searchParams = useSearchParams()
const [activeTab, setTab] = useState<string>(
!disableSearchParams
@ -37,7 +39,7 @@ export const useTabSearchParams = ({
setTab(newActiveTab)
if (disableSearchParams)
return
history[`${routingBehavior}State`](null, '', `${pathName}?${searchParamName}=${newActiveTab}`)
router[`${routingBehavior}`](`${pathName}?${searchParamName}=${newActiveTab}`)
}
return [activeTab, setActiveTab] as const

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',
@ -70,10 +70,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.',
},
@ -95,7 +95,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

@ -167,9 +167,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

@ -7,7 +7,7 @@ const translation = {
des: 'Upgrade deinen Plan, 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,28 +30,28 @@ const translation = {
overview: {
title: 'Overview',
appInfo: {
explanation: 'Ready-to-use AI WebApp',
explanation: 'Ready-to-use AI web app',
accessibleAddress: 'Public URL',
preview: 'Preview',
launch: 'Launch',
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',
@ -60,14 +60,14 @@ const translation = {
invalidPrivacyPolicy: 'Invalid privacy policy link. Please use a valid link that starts with http or https',
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',
@ -96,7 +96,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 ',
@ -195,6 +195,41 @@ const translation = {
modelNotSupported: 'Model not supported',
modelNotSupportedTip: 'The current model does not support this feature and is automatically downgraded to prompt injection.',
},
accessControl: 'Web App Access Control',
accessItemsDescription: {
anyone: 'Anyone can access the web app',
specific: 'Only specific groups or members can access the web app',
organization: 'Anyone in the organization can access the web app',
},
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

@ -147,6 +147,8 @@ const translation = {
status: 'beta',
explore: 'Explore',
apps: 'Studio',
appDetail: 'App Detail',
account: 'Account',
plugins: 'Plugins',
exploreMarketplace: 'Explore Marketplace',
pluginsTips: 'Integrate third-party plugins or create ChatGPT-compatible AI-Plugins.',
@ -196,7 +198,7 @@ const translation = {
account: {
account: 'Account',
myAccount: 'My Account',
studio: 'Dify Studio',
studio: 'Studio',
avatar: 'Avatar',
name: 'Name',
email: 'Email',
@ -208,8 +210,8 @@ const translation = {
newPassword: 'New password',
confirmPassword: 'Confirm password',
notEqual: 'Two passwords are different.',
langGeniusAccount: 'Dify account',
langGeniusAccountTip: 'Your Dify account and associated user data.',
langGeniusAccount: 'Account\'s data',
langGeniusAccountTip: 'The user data of your account.',
editName: 'Edit Name',
showAppLength: 'Show {{length}} apps',
delete: 'Delete Account',
@ -657,6 +659,7 @@ const translation = {
license: {
expiring: 'Expiring in one day',
expiring_plural: 'Expiring in {{count}} days',
unlimited: 'Unlimited',
},
pagination: {
perPage: 'Items per page',
@ -666,6 +669,7 @@ const translation = {
browse: 'browse',
supportedFormats: 'Supports PNG, JPG, JPEG, WEBP and GIF',
},
you: 'You',
}
export default translation

View File

@ -7,7 +7,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

@ -16,7 +16,7 @@ const translation = {
},
},
apps: {
title: 'Explore Apps by Dify',
title: 'Explore Apps',
description: 'Use these template apps instantly or customize your own apps based on the templates.',
allCategories: 'Recommended',
},

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',
@ -70,10 +70,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

@ -7,7 +7,7 @@ const translation = {
title: 'Actualiza tu plan',
},
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: 'تم رنگی چت',
@ -70,10 +70,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: 'تنظیمات برنامه وب سمت مشتری.',
},
@ -95,7 +95,7 @@ const translation = {
customize: {
way: 'راه',
entry: 'سفارشی‌سازی',
title: 'سفارشی‌سازی WebApp AI',
title: 'سفارشی‌سازی web app AI',
explanation: 'شما می‌توانید ظاهر جلویی برنامه وب را برای برآوردن نیازهای سناریو و سبک خود سفارشی کنید.',
way1: {
name: 'کلاینت را شاخه کنید، آن را تغییر دهید و در Vercel مستقر کنید (توصیه می‌شود)',

View File

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

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