dify
This commit is contained in:
34
dify/web/context/access-control-store.ts
Normal file
34
dify/web/context/access-control-store.ts
Normal 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
|
||||
188
dify/web/context/app-context.tsx
Normal file
188
dify/web/context/app-context.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { createContext, useContext, useContextSelector } from 'use-context-selector'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { fetchCurrentWorkspace, fetchLangGeniusVersion, fetchUserProfile } from '@/service/common'
|
||||
import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
|
||||
import MaintenanceNotice from '@/app/components/header/maintenance-notice'
|
||||
import { noop } from 'lodash-es'
|
||||
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
|
||||
import { ZENDESK_FIELD_IDS } from '@/config'
|
||||
import { useGlobalPublicStore } from './global-public-context'
|
||||
|
||||
export type AppContextValue = {
|
||||
userProfile: UserProfileResponse
|
||||
mutateUserProfile: VoidFunction
|
||||
currentWorkspace: ICurrentWorkspace
|
||||
isCurrentWorkspaceManager: boolean
|
||||
isCurrentWorkspaceOwner: boolean
|
||||
isCurrentWorkspaceEditor: boolean
|
||||
isCurrentWorkspaceDatasetOperator: boolean
|
||||
mutateCurrentWorkspace: VoidFunction
|
||||
langGeniusVersionInfo: LangGeniusVersionResponse
|
||||
useSelector: typeof useSelector
|
||||
isLoadingCurrentWorkspace: boolean
|
||||
}
|
||||
|
||||
const userProfilePlaceholder = {
|
||||
id: '',
|
||||
name: '',
|
||||
email: '',
|
||||
avatar: '',
|
||||
avatar_url: '',
|
||||
is_password_set: false,
|
||||
}
|
||||
|
||||
const initialLangGeniusVersionInfo = {
|
||||
current_env: '',
|
||||
current_version: '',
|
||||
latest_version: '',
|
||||
release_date: '',
|
||||
release_notes: '',
|
||||
version: '',
|
||||
can_auto_update: false,
|
||||
}
|
||||
|
||||
const initialWorkspaceInfo: ICurrentWorkspace = {
|
||||
id: '',
|
||||
name: '',
|
||||
plan: '',
|
||||
status: '',
|
||||
created_at: 0,
|
||||
role: 'normal',
|
||||
providers: [],
|
||||
}
|
||||
|
||||
const AppContext = createContext<AppContextValue>({
|
||||
userProfile: userProfilePlaceholder,
|
||||
currentWorkspace: initialWorkspaceInfo,
|
||||
isCurrentWorkspaceManager: false,
|
||||
isCurrentWorkspaceOwner: false,
|
||||
isCurrentWorkspaceEditor: false,
|
||||
isCurrentWorkspaceDatasetOperator: false,
|
||||
mutateUserProfile: noop,
|
||||
mutateCurrentWorkspace: noop,
|
||||
langGeniusVersionInfo: initialLangGeniusVersionInfo,
|
||||
useSelector,
|
||||
isLoadingCurrentWorkspace: false,
|
||||
})
|
||||
|
||||
export function useSelector<T>(selector: (value: AppContextValue) => T): T {
|
||||
return useContextSelector(AppContext, selector)
|
||||
}
|
||||
|
||||
export type AppContextProviderProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) => {
|
||||
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const { data: userProfileResponse, mutate: mutateUserProfile, error: userProfileError } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile)
|
||||
const { data: currentWorkspaceResponse, mutate: mutateCurrentWorkspace, isLoading: isLoadingCurrentWorkspace } = useSWR({ url: '/workspaces/current', params: {} }, fetchCurrentWorkspace)
|
||||
|
||||
const [userProfile, setUserProfile] = useState<UserProfileResponse>(userProfilePlaceholder)
|
||||
const [langGeniusVersionInfo, setLangGeniusVersionInfo] = useState<LangGeniusVersionResponse>(initialLangGeniusVersionInfo)
|
||||
const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo)
|
||||
const isCurrentWorkspaceManager = useMemo(() => ['owner', 'admin'].includes(currentWorkspace.role), [currentWorkspace.role])
|
||||
const isCurrentWorkspaceOwner = useMemo(() => currentWorkspace.role === 'owner', [currentWorkspace.role])
|
||||
const isCurrentWorkspaceEditor = useMemo(() => ['owner', 'admin', 'editor'].includes(currentWorkspace.role), [currentWorkspace.role])
|
||||
const isCurrentWorkspaceDatasetOperator = useMemo(() => currentWorkspace.role === 'dataset_operator', [currentWorkspace.role])
|
||||
const updateUserProfileAndVersion = useCallback(async () => {
|
||||
if (userProfileResponse && !userProfileResponse.bodyUsed) {
|
||||
try {
|
||||
const result = await userProfileResponse.json()
|
||||
setUserProfile(result)
|
||||
if (!systemFeatures.branding.enabled) {
|
||||
const current_version = userProfileResponse.headers.get('x-version')
|
||||
const current_env = process.env.NODE_ENV === 'development' ? 'DEVELOPMENT' : userProfileResponse.headers.get('x-env')
|
||||
const versionData = await fetchLangGeniusVersion({ url: '/version', params: { current_version } })
|
||||
setLangGeniusVersionInfo({ ...versionData, current_version, latest_version: versionData.version, current_env })
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to update user profile:', error)
|
||||
if (userProfile.id === '')
|
||||
setUserProfile(userProfilePlaceholder)
|
||||
}
|
||||
}
|
||||
else if (userProfileError && userProfile.id === '') {
|
||||
setUserProfile(userProfilePlaceholder)
|
||||
}
|
||||
}, [userProfileResponse, userProfileError, userProfile.id])
|
||||
|
||||
useEffect(() => {
|
||||
updateUserProfileAndVersion()
|
||||
}, [updateUserProfileAndVersion, userProfileResponse])
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspaceResponse)
|
||||
setCurrentWorkspace(currentWorkspaceResponse)
|
||||
}, [currentWorkspaceResponse])
|
||||
|
||||
// #region Zendesk conversation fields
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.ENVIRONMENT && langGeniusVersionInfo?.current_env) {
|
||||
setZendeskConversationFields([{
|
||||
id: ZENDESK_FIELD_IDS.ENVIRONMENT,
|
||||
value: langGeniusVersionInfo.current_env.toLowerCase(),
|
||||
}])
|
||||
}
|
||||
}, [langGeniusVersionInfo?.current_env])
|
||||
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.VERSION && langGeniusVersionInfo?.version) {
|
||||
setZendeskConversationFields([{
|
||||
id: ZENDESK_FIELD_IDS.VERSION,
|
||||
value: langGeniusVersionInfo.version,
|
||||
}])
|
||||
}
|
||||
}, [langGeniusVersionInfo?.version])
|
||||
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.EMAIL && userProfile?.email) {
|
||||
setZendeskConversationFields([{
|
||||
id: ZENDESK_FIELD_IDS.EMAIL,
|
||||
value: userProfile.email,
|
||||
}])
|
||||
}
|
||||
}, [userProfile?.email])
|
||||
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.WORKSPACE_ID && currentWorkspace?.id) {
|
||||
setZendeskConversationFields([{
|
||||
id: ZENDESK_FIELD_IDS.WORKSPACE_ID,
|
||||
value: currentWorkspace.id,
|
||||
}])
|
||||
}
|
||||
}, [currentWorkspace?.id])
|
||||
// #endregion Zendesk conversation fields
|
||||
|
||||
return (
|
||||
<AppContext.Provider value={{
|
||||
userProfile,
|
||||
mutateUserProfile,
|
||||
langGeniusVersionInfo,
|
||||
useSelector,
|
||||
currentWorkspace,
|
||||
isCurrentWorkspaceManager,
|
||||
isCurrentWorkspaceOwner,
|
||||
isCurrentWorkspaceEditor,
|
||||
isCurrentWorkspaceDatasetOperator,
|
||||
mutateCurrentWorkspace,
|
||||
isLoadingCurrentWorkspace,
|
||||
}}>
|
||||
<div className='flex h-full flex-col overflow-y-auto'>
|
||||
{globalThis.document?.body?.getAttribute('data-public-maintenance-notice') && <MaintenanceNotice />}
|
||||
<div className='relative flex grow flex-col overflow-y-auto overflow-x-hidden bg-background-body'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</AppContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useAppContext = () => useContext(AppContext)
|
||||
|
||||
export default AppContext
|
||||
18
dify/web/context/dataset-detail.ts
Normal file
18
dify/web/context/dataset-detail.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createContext, useContext, useContextSelector } from 'use-context-selector'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import type { IndexingType } from '@/app/components/datasets/create/step-two'
|
||||
import type { QueryObserverResult, RefetchOptions } from '@tanstack/react-query'
|
||||
|
||||
type DatasetDetailContextValue = {
|
||||
indexingTechnique?: IndexingType
|
||||
dataset?: DataSet
|
||||
mutateDatasetRes?: (options?: RefetchOptions | undefined) => Promise<QueryObserverResult<DataSet, Error>>
|
||||
}
|
||||
const DatasetDetailContext = createContext<DatasetDetailContextValue>({})
|
||||
|
||||
export const useDatasetDetailContext = () => useContext(DatasetDetailContext)
|
||||
|
||||
export const useDatasetDetailContextWithSelector = <T>(selector: (value: DatasetDetailContextValue) => T): T => {
|
||||
return useContextSelector(DatasetDetailContext, selector)
|
||||
}
|
||||
export default DatasetDetailContext
|
||||
21
dify/web/context/datasets-context.tsx
Normal file
21
dify/web/context/datasets-context.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
export type DatasetsContextValue = {
|
||||
datasets: DataSet[]
|
||||
mutateDatasets: () => void
|
||||
currentDataset?: DataSet
|
||||
}
|
||||
|
||||
const DatasetsContext = createContext<DatasetsContextValue>({
|
||||
datasets: [],
|
||||
mutateDatasets: noop,
|
||||
currentDataset: undefined,
|
||||
})
|
||||
|
||||
export const useDatasetsContext = () => useContext(DatasetsContext)
|
||||
|
||||
export default DatasetsContext
|
||||
276
dify/web/context/debug-configuration.ts
Normal file
276
dify/web/context/debug-configuration.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import type { RefObject } from 'react'
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
import { PromptMode } from '@/models/debug'
|
||||
import type {
|
||||
AnnotationReplyConfig,
|
||||
BlockStatus,
|
||||
ChatPromptConfig,
|
||||
CitationConfig,
|
||||
CompletionPromptConfig,
|
||||
ConversationHistoriesRole,
|
||||
DatasetConfigs,
|
||||
Inputs,
|
||||
ModelConfig,
|
||||
ModerationConfig,
|
||||
MoreLikeThisConfig,
|
||||
PromptConfig,
|
||||
PromptItem,
|
||||
SpeechToTextConfig,
|
||||
SuggestedQuestionsAfterAnswerConfig,
|
||||
TextToSpeechConfig,
|
||||
} from '@/models/debug'
|
||||
import type { ExternalDataTool } from '@/models/common'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import type { VisionSettings } from '@/types/app'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { ModelModeType, RETRIEVE_TYPE, Resolution, TransferMethod } from '@/types/app'
|
||||
import { ANNOTATION_DEFAULT, DEFAULT_AGENT_SETTING, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
|
||||
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { Collection } from '@/app/components/tools/types'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
type IDebugConfiguration = {
|
||||
appId: string
|
||||
isAPIKeySet: boolean
|
||||
isTrailFinished: boolean
|
||||
mode: AppModeEnum
|
||||
modelModeType: ModelModeType
|
||||
promptMode: PromptMode
|
||||
setPromptMode: (promptMode: PromptMode) => void
|
||||
isAdvancedMode: boolean
|
||||
isAgent: boolean
|
||||
isFunctionCall: boolean
|
||||
isOpenAI: boolean
|
||||
collectionList: Collection[]
|
||||
canReturnToSimpleMode: boolean
|
||||
setCanReturnToSimpleMode: (canReturnToSimpleMode: boolean) => void
|
||||
chatPromptConfig: ChatPromptConfig
|
||||
completionPromptConfig: CompletionPromptConfig
|
||||
currentAdvancedPrompt: PromptItem | PromptItem[]
|
||||
setCurrentAdvancedPrompt: (prompt: PromptItem | PromptItem[], isUserChanged?: boolean) => void
|
||||
showHistoryModal: () => void
|
||||
conversationHistoriesRole: ConversationHistoriesRole
|
||||
setConversationHistoriesRole: (conversationHistoriesRole: ConversationHistoriesRole) => void
|
||||
hasSetBlockStatus: BlockStatus
|
||||
conversationId: string | null // after first chat send
|
||||
setConversationId: (conversationId: string | null) => void
|
||||
introduction: string
|
||||
setIntroduction: (introduction: string) => void
|
||||
suggestedQuestions: string[]
|
||||
setSuggestedQuestions: (questions: string[]) => void
|
||||
controlClearChatMessage: number
|
||||
setControlClearChatMessage: (controlClearChatMessage: number) => void
|
||||
prevPromptConfig: PromptConfig
|
||||
setPrevPromptConfig: (prevPromptConfig: PromptConfig) => void
|
||||
moreLikeThisConfig: MoreLikeThisConfig
|
||||
setMoreLikeThisConfig: (moreLikeThisConfig: MoreLikeThisConfig) => void
|
||||
suggestedQuestionsAfterAnswerConfig: SuggestedQuestionsAfterAnswerConfig
|
||||
setSuggestedQuestionsAfterAnswerConfig: (suggestedQuestionsAfterAnswerConfig: SuggestedQuestionsAfterAnswerConfig) => void
|
||||
speechToTextConfig: SpeechToTextConfig
|
||||
setSpeechToTextConfig: (speechToTextConfig: SpeechToTextConfig) => void
|
||||
textToSpeechConfig: TextToSpeechConfig
|
||||
setTextToSpeechConfig: (textToSpeechConfig: TextToSpeechConfig) => void
|
||||
citationConfig: CitationConfig
|
||||
setCitationConfig: (citationConfig: CitationConfig) => void
|
||||
annotationConfig: AnnotationReplyConfig
|
||||
setAnnotationConfig: (annotationConfig: AnnotationReplyConfig) => void
|
||||
moderationConfig: ModerationConfig
|
||||
setModerationConfig: (moderationConfig: ModerationConfig) => void
|
||||
externalDataToolsConfig: ExternalDataTool[]
|
||||
setExternalDataToolsConfig: (externalDataTools: ExternalDataTool[]) => void
|
||||
formattingChanged: boolean
|
||||
setFormattingChanged: (formattingChanged: boolean) => void
|
||||
inputs: Inputs
|
||||
setInputs: (inputs: Inputs) => void
|
||||
query: string // user question
|
||||
setQuery: (query: string) => void
|
||||
// Belows are draft infos
|
||||
completionParams: FormValue
|
||||
setCompletionParams: (completionParams: FormValue) => void
|
||||
// model_config
|
||||
modelConfig: ModelConfig
|
||||
setModelConfig: (modelConfig: ModelConfig) => void
|
||||
dataSets: DataSet[]
|
||||
setDataSets: (dataSet: DataSet[]) => void
|
||||
showSelectDataSet: () => void
|
||||
// dataset config
|
||||
datasetConfigs: DatasetConfigs
|
||||
datasetConfigsRef: RefObject<DatasetConfigs>
|
||||
setDatasetConfigs: (config: DatasetConfigs) => void
|
||||
hasSetContextVar: boolean
|
||||
isShowVisionConfig: boolean
|
||||
visionConfig: VisionSettings
|
||||
setVisionConfig: (visionConfig: VisionSettings, noNotice?: boolean) => void
|
||||
isAllowVideoUpload: boolean
|
||||
isShowDocumentConfig: boolean
|
||||
isShowAudioConfig: boolean
|
||||
rerankSettingModalOpen: boolean
|
||||
setRerankSettingModalOpen: (rerankSettingModalOpen: boolean) => void
|
||||
}
|
||||
|
||||
const DebugConfigurationContext = createContext<IDebugConfiguration>({
|
||||
appId: '',
|
||||
isAPIKeySet: false,
|
||||
isTrailFinished: false,
|
||||
mode: AppModeEnum.CHAT,
|
||||
modelModeType: ModelModeType.chat,
|
||||
promptMode: PromptMode.simple,
|
||||
setPromptMode: noop,
|
||||
isAdvancedMode: false,
|
||||
isAgent: false,
|
||||
isFunctionCall: false,
|
||||
isOpenAI: false,
|
||||
collectionList: [],
|
||||
canReturnToSimpleMode: false,
|
||||
setCanReturnToSimpleMode: noop,
|
||||
chatPromptConfig: DEFAULT_CHAT_PROMPT_CONFIG,
|
||||
completionPromptConfig: DEFAULT_COMPLETION_PROMPT_CONFIG,
|
||||
currentAdvancedPrompt: [],
|
||||
showHistoryModal: noop,
|
||||
conversationHistoriesRole: {
|
||||
user_prefix: 'user',
|
||||
assistant_prefix: 'assistant',
|
||||
},
|
||||
setConversationHistoriesRole: noop,
|
||||
setCurrentAdvancedPrompt: noop,
|
||||
hasSetBlockStatus: {
|
||||
context: false,
|
||||
history: false,
|
||||
query: false,
|
||||
},
|
||||
conversationId: '',
|
||||
setConversationId: noop,
|
||||
introduction: '',
|
||||
setIntroduction: noop,
|
||||
suggestedQuestions: [],
|
||||
setSuggestedQuestions: noop,
|
||||
controlClearChatMessage: 0,
|
||||
setControlClearChatMessage: noop,
|
||||
prevPromptConfig: {
|
||||
prompt_template: '',
|
||||
prompt_variables: [],
|
||||
},
|
||||
setPrevPromptConfig: noop,
|
||||
moreLikeThisConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
setMoreLikeThisConfig: noop,
|
||||
suggestedQuestionsAfterAnswerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
setSuggestedQuestionsAfterAnswerConfig: noop,
|
||||
speechToTextConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
setSpeechToTextConfig: noop,
|
||||
textToSpeechConfig: {
|
||||
enabled: false,
|
||||
voice: '',
|
||||
language: '',
|
||||
},
|
||||
setTextToSpeechConfig: noop,
|
||||
citationConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
setCitationConfig: noop,
|
||||
moderationConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
annotationConfig: {
|
||||
id: '',
|
||||
enabled: false,
|
||||
score_threshold: ANNOTATION_DEFAULT.score_threshold,
|
||||
embedding_model: {
|
||||
embedding_model_name: '',
|
||||
embedding_provider_name: '',
|
||||
},
|
||||
},
|
||||
setAnnotationConfig: noop,
|
||||
setModerationConfig: noop,
|
||||
externalDataToolsConfig: [],
|
||||
setExternalDataToolsConfig: noop,
|
||||
formattingChanged: false,
|
||||
setFormattingChanged: noop,
|
||||
inputs: {},
|
||||
setInputs: noop,
|
||||
query: '',
|
||||
setQuery: noop,
|
||||
completionParams: {
|
||||
max_tokens: 16,
|
||||
temperature: 1, // 0-2
|
||||
top_p: 1,
|
||||
presence_penalty: 1, // -2-2
|
||||
frequency_penalty: 1, // -2-2
|
||||
},
|
||||
setCompletionParams: noop,
|
||||
modelConfig: {
|
||||
provider: 'OPENAI', // 'OPENAI'
|
||||
model_id: 'gpt-3.5-turbo', // 'gpt-3.5-turbo'
|
||||
mode: ModelModeType.unset,
|
||||
configs: {
|
||||
prompt_template: '',
|
||||
prompt_variables: [],
|
||||
},
|
||||
chat_prompt_config: DEFAULT_CHAT_PROMPT_CONFIG,
|
||||
completion_prompt_config: DEFAULT_COMPLETION_PROMPT_CONFIG,
|
||||
more_like_this: null,
|
||||
opening_statement: '',
|
||||
suggested_questions: [],
|
||||
sensitive_word_avoidance: null,
|
||||
speech_to_text: null,
|
||||
text_to_speech: null,
|
||||
file_upload: null,
|
||||
suggested_questions_after_answer: null,
|
||||
retriever_resource: null,
|
||||
annotation_reply: null,
|
||||
external_data_tools: [],
|
||||
system_parameters: {
|
||||
audio_file_size_limit: 0,
|
||||
file_size_limit: 0,
|
||||
image_file_size_limit: 0,
|
||||
video_file_size_limit: 0,
|
||||
workflow_file_upload_limit: 0,
|
||||
},
|
||||
dataSets: [],
|
||||
agentConfig: DEFAULT_AGENT_SETTING,
|
||||
},
|
||||
setModelConfig: noop,
|
||||
dataSets: [],
|
||||
showSelectDataSet: noop,
|
||||
setDataSets: noop,
|
||||
datasetConfigs: {
|
||||
retrieval_model: RETRIEVE_TYPE.multiWay,
|
||||
reranking_model: {
|
||||
reranking_provider_name: '',
|
||||
reranking_model_name: '',
|
||||
},
|
||||
top_k: 4,
|
||||
score_threshold_enabled: false,
|
||||
score_threshold: 0.7,
|
||||
datasets: {
|
||||
datasets: [],
|
||||
},
|
||||
},
|
||||
datasetConfigsRef: {
|
||||
current: null,
|
||||
} as unknown as RefObject<DatasetConfigs>,
|
||||
setDatasetConfigs: noop,
|
||||
hasSetContextVar: false,
|
||||
isShowVisionConfig: false,
|
||||
visionConfig: {
|
||||
enabled: false,
|
||||
number_limits: 2,
|
||||
detail: Resolution.low,
|
||||
transfer_methods: [TransferMethod.remote_url],
|
||||
},
|
||||
setVisionConfig: noop,
|
||||
isAllowVideoUpload: false,
|
||||
isShowDocumentConfig: false,
|
||||
isShowAudioConfig: false,
|
||||
rerankSettingModalOpen: false,
|
||||
setRerankSettingModalOpen: noop,
|
||||
})
|
||||
|
||||
export const useDebugConfigurationContext = () => useContext(DebugConfigurationContext)
|
||||
|
||||
export default DebugConfigurationContext
|
||||
28
dify/web/context/event-emitter.tsx
Normal file
28
dify/web/context/event-emitter.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
import { useEventEmitter } from 'ahooks'
|
||||
import type { EventEmitter } from 'ahooks/lib/useEventEmitter'
|
||||
|
||||
const EventEmitterContext = createContext<{ eventEmitter: EventEmitter<string> | null }>({
|
||||
eventEmitter: null,
|
||||
})
|
||||
|
||||
export const useEventEmitterContextContext = () => useContext(EventEmitterContext)
|
||||
|
||||
type EventEmitterContextProviderProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
export const EventEmitterContextProvider = ({
|
||||
children,
|
||||
}: EventEmitterContextProviderProps) => {
|
||||
const eventEmitter = useEventEmitter<string>()
|
||||
|
||||
return (
|
||||
<EventEmitterContext.Provider value={{ eventEmitter }}>
|
||||
{children}
|
||||
</EventEmitterContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default EventEmitterContext
|
||||
25
dify/web/context/explore-context.ts
Normal file
25
dify/web/context/explore-context.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { createContext } from 'use-context-selector'
|
||||
import type { InstalledApp } from '@/models/explore'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
type IExplore = {
|
||||
controlUpdateInstalledApps: number
|
||||
setControlUpdateInstalledApps: (controlUpdateInstalledApps: number) => void
|
||||
hasEditPermission: boolean
|
||||
installedApps: InstalledApp[]
|
||||
setInstalledApps: (installedApps: InstalledApp[]) => void
|
||||
isFetchingInstalledApps: boolean
|
||||
setIsFetchingInstalledApps: (isFetchingInstalledApps: boolean) => void
|
||||
}
|
||||
|
||||
const ExploreContext = createContext<IExplore>({
|
||||
controlUpdateInstalledApps: 0,
|
||||
setControlUpdateInstalledApps: noop,
|
||||
hasEditPermission: false,
|
||||
installedApps: [],
|
||||
setInstalledApps: noop,
|
||||
isFetchingInstalledApps: false,
|
||||
setIsFetchingInstalledApps: noop,
|
||||
})
|
||||
|
||||
export default ExploreContext
|
||||
28
dify/web/context/external-api-panel-context.tsx
Normal file
28
dify/web/context/external-api-panel-context.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client'
|
||||
|
||||
import React, { createContext, useContext, useState } from 'react'
|
||||
|
||||
type ExternalApiPanelContextType = {
|
||||
showExternalApiPanel: boolean
|
||||
setShowExternalApiPanel: (show: boolean) => void
|
||||
}
|
||||
|
||||
const ExternalApiPanelContext = createContext<ExternalApiPanelContextType | undefined>(undefined)
|
||||
|
||||
export const ExternalApiPanelProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [showExternalApiPanel, setShowExternalApiPanel] = useState(false)
|
||||
|
||||
return (
|
||||
<ExternalApiPanelContext.Provider value={{ showExternalApiPanel, setShowExternalApiPanel }}>
|
||||
{children}
|
||||
</ExternalApiPanelContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useExternalApiPanel = () => {
|
||||
const context = useContext(ExternalApiPanelContext)
|
||||
if (context === undefined)
|
||||
throw new Error('useExternalApiPanel must be used within an ExternalApiPanelProvider')
|
||||
|
||||
return context
|
||||
}
|
||||
46
dify/web/context/external-knowledge-api-context.tsx
Normal file
46
dify/web/context/external-knowledge-api-context.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useMemo } from 'react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import type { ExternalAPIItem, ExternalAPIListResponse } from '@/models/datasets'
|
||||
import { fetchExternalAPIList } from '@/service/datasets'
|
||||
|
||||
type ExternalKnowledgeApiContextType = {
|
||||
externalKnowledgeApiList: ExternalAPIItem[]
|
||||
mutateExternalKnowledgeApis: () => Promise<ExternalAPIListResponse | undefined>
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
const ExternalKnowledgeApiContext = createContext<ExternalKnowledgeApiContextType | undefined>(undefined)
|
||||
|
||||
export type ExternalKnowledgeApiProviderProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const ExternalKnowledgeApiProvider: FC<ExternalKnowledgeApiProviderProps> = ({ children }) => {
|
||||
const { data, mutate: mutateExternalKnowledgeApis, isLoading } = useSWR<ExternalAPIListResponse>(
|
||||
{ url: '/datasets/external-knowledge-api' },
|
||||
fetchExternalAPIList,
|
||||
)
|
||||
|
||||
const contextValue = useMemo<ExternalKnowledgeApiContextType>(() => ({
|
||||
externalKnowledgeApiList: data?.data || [],
|
||||
mutateExternalKnowledgeApis,
|
||||
isLoading,
|
||||
}), [data, mutateExternalKnowledgeApis, isLoading])
|
||||
|
||||
return (
|
||||
<ExternalKnowledgeApiContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</ExternalKnowledgeApiContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useExternalKnowledgeApi = () => {
|
||||
const context = useContext(ExternalKnowledgeApiContext)
|
||||
if (context === undefined)
|
||||
throw new Error('useExternalKnowledgeApi must be used within a ExternalKnowledgeApiProvider')
|
||||
|
||||
return context
|
||||
}
|
||||
46
dify/web/context/global-public-context.tsx
Normal file
46
dify/web/context/global-public-context.tsx
Normal 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 = {
|
||||
isGlobalPending: boolean
|
||||
setIsGlobalPending: (isPending: boolean) => void
|
||||
systemFeatures: SystemFeatures
|
||||
setSystemFeatures: (systemFeatures: SystemFeatures) => void
|
||||
}
|
||||
|
||||
export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({
|
||||
isGlobalPending: true,
|
||||
setIsGlobalPending: (isPending: boolean) => set(() => ({ isGlobalPending: isPending })),
|
||||
systemFeatures: defaultSystemFeatures,
|
||||
setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })),
|
||||
}))
|
||||
|
||||
const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { isPending, data } = useQuery({
|
||||
queryKey: ['systemFeatures'],
|
||||
queryFn: getSystemFeatures,
|
||||
})
|
||||
const { setSystemFeatures, setIsGlobalPending: 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
|
||||
130
dify/web/context/hooks/use-trigger-events-limit-modal.ts
Normal file
130
dify/web/context/hooks/use-trigger-events-limit-modal.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { type Dispatch, type SetStateAction, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { NUM_INFINITE } from '@/app/components/billing/config'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import type { ModalState } from '../modal-context'
|
||||
|
||||
export type TriggerEventsLimitModalPayload = {
|
||||
usage: number
|
||||
total: number
|
||||
resetInDays?: number
|
||||
planType: Plan
|
||||
storageKey?: string
|
||||
persistDismiss?: boolean
|
||||
}
|
||||
|
||||
type TriggerPlanInfo = {
|
||||
type: Plan
|
||||
usage: { triggerEvents: number }
|
||||
total: { triggerEvents: number }
|
||||
reset: { triggerEvents?: number | null }
|
||||
}
|
||||
|
||||
type UseTriggerEventsLimitModalOptions = {
|
||||
plan: TriggerPlanInfo
|
||||
isFetchedPlan: boolean
|
||||
currentWorkspaceId?: string
|
||||
}
|
||||
|
||||
type UseTriggerEventsLimitModalResult = {
|
||||
showTriggerEventsLimitModal: ModalState<TriggerEventsLimitModalPayload> | null
|
||||
setShowTriggerEventsLimitModal: Dispatch<SetStateAction<ModalState<TriggerEventsLimitModalPayload> | null>>
|
||||
persistTriggerEventsLimitModalDismiss: () => void
|
||||
}
|
||||
|
||||
const TRIGGER_EVENTS_LOCALSTORAGE_PREFIX = 'trigger-events-limit-dismissed'
|
||||
|
||||
export const useTriggerEventsLimitModal = ({
|
||||
plan,
|
||||
isFetchedPlan,
|
||||
currentWorkspaceId,
|
||||
}: UseTriggerEventsLimitModalOptions): UseTriggerEventsLimitModalResult => {
|
||||
const [showTriggerEventsLimitModal, setShowTriggerEventsLimitModal] = useState<ModalState<TriggerEventsLimitModalPayload> | null>(null)
|
||||
const dismissedTriggerEventsLimitStorageKeysRef = useRef<Record<string, boolean>>({})
|
||||
|
||||
useEffect(() => {
|
||||
if (!IS_CLOUD_EDITION)
|
||||
return
|
||||
if (typeof window === 'undefined')
|
||||
return
|
||||
if (!currentWorkspaceId)
|
||||
return
|
||||
if (!isFetchedPlan) {
|
||||
setShowTriggerEventsLimitModal(null)
|
||||
return
|
||||
}
|
||||
|
||||
const { type, usage, total, reset } = plan
|
||||
const isUnlimited = total.triggerEvents === NUM_INFINITE
|
||||
const reachedLimit = total.triggerEvents > 0 && usage.triggerEvents >= total.triggerEvents
|
||||
|
||||
if (type === Plan.team || isUnlimited || !reachedLimit) {
|
||||
if (showTriggerEventsLimitModal)
|
||||
setShowTriggerEventsLimitModal(null)
|
||||
return
|
||||
}
|
||||
|
||||
const triggerResetInDays = type === Plan.professional && total.triggerEvents !== NUM_INFINITE
|
||||
? reset.triggerEvents ?? undefined
|
||||
: undefined
|
||||
const cycleTag = (() => {
|
||||
if (typeof reset.triggerEvents === 'number')
|
||||
return dayjs().startOf('day').add(reset.triggerEvents, 'day').format('YYYY-MM-DD')
|
||||
if (type === Plan.sandbox)
|
||||
return dayjs().endOf('month').format('YYYY-MM-DD')
|
||||
return 'none'
|
||||
})()
|
||||
const storageKey = `${TRIGGER_EVENTS_LOCALSTORAGE_PREFIX}-${currentWorkspaceId}-${type}-${total.triggerEvents}-${cycleTag}`
|
||||
if (dismissedTriggerEventsLimitStorageKeysRef.current[storageKey])
|
||||
return
|
||||
|
||||
let persistDismiss = true
|
||||
let hasDismissed = false
|
||||
try {
|
||||
if (localStorage.getItem(storageKey) === '1')
|
||||
hasDismissed = true
|
||||
}
|
||||
catch {
|
||||
persistDismiss = false
|
||||
}
|
||||
if (hasDismissed)
|
||||
return
|
||||
|
||||
if (showTriggerEventsLimitModal?.payload.storageKey === storageKey)
|
||||
return
|
||||
|
||||
setShowTriggerEventsLimitModal({
|
||||
payload: {
|
||||
usage: usage.triggerEvents,
|
||||
total: total.triggerEvents,
|
||||
planType: type,
|
||||
resetInDays: triggerResetInDays,
|
||||
storageKey,
|
||||
persistDismiss,
|
||||
},
|
||||
})
|
||||
}, [plan, isFetchedPlan, showTriggerEventsLimitModal, currentWorkspaceId])
|
||||
|
||||
const persistTriggerEventsLimitModalDismiss = useCallback(() => {
|
||||
const storageKey = showTriggerEventsLimitModal?.payload.storageKey
|
||||
if (!storageKey)
|
||||
return
|
||||
if (showTriggerEventsLimitModal?.payload.persistDismiss) {
|
||||
try {
|
||||
localStorage.setItem(storageKey, '1')
|
||||
return
|
||||
}
|
||||
catch {
|
||||
// ignore error and fall back to in-memory guard
|
||||
}
|
||||
}
|
||||
dismissedTriggerEventsLimitStorageKeysRef.current[storageKey] = true
|
||||
}, [showTriggerEventsLimitModal])
|
||||
|
||||
return {
|
||||
showTriggerEventsLimitModal,
|
||||
setShowTriggerEventsLimitModal,
|
||||
persistTriggerEventsLimitModalDismiss,
|
||||
}
|
||||
}
|
||||
48
dify/web/context/i18n.ts
Normal file
48
dify/web/context/i18n.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
} from 'use-context-selector'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { getDocLanguage, getLanguage, getPricingPageLanguage } from '@/i18n-config/language'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
type II18NContext = {
|
||||
locale: Locale
|
||||
i18n: Record<string, any>
|
||||
setLocaleOnClient: (_lang: Locale, _reloadPage?: boolean) => Promise<void>
|
||||
}
|
||||
|
||||
const I18NContext = createContext<II18NContext>({
|
||||
locale: 'en-US',
|
||||
i18n: {},
|
||||
setLocaleOnClient: async (_lang: Locale, _reloadPage?: boolean) => {
|
||||
noop()
|
||||
},
|
||||
})
|
||||
|
||||
export const useI18N = () => useContext(I18NContext)
|
||||
export const useGetLanguage = () => {
|
||||
const { locale } = useI18N()
|
||||
|
||||
return getLanguage(locale)
|
||||
}
|
||||
export const useGetPricingPageLanguage = () => {
|
||||
const { locale } = useI18N()
|
||||
|
||||
return getPricingPageLanguage(locale)
|
||||
}
|
||||
|
||||
export const defaultDocBaseUrl = 'https://docs.dify.ai'
|
||||
export const useDocLink = (baseUrl?: string): ((path?: string, pathMap?: { [index: string]: string }) => string) => {
|
||||
let baseDocUrl = baseUrl || defaultDocBaseUrl
|
||||
baseDocUrl = (baseDocUrl.endsWith('/')) ? baseDocUrl.slice(0, -1) : baseDocUrl
|
||||
const { locale } = useI18N()
|
||||
const docLanguage = getDocLanguage(locale)
|
||||
return (path?: string, pathMap?: { [index: string]: string }): string => {
|
||||
const pathUrl = path || ''
|
||||
let targetPath = (pathMap) ? pathMap[locale] || pathUrl : pathUrl
|
||||
targetPath = (targetPath.startsWith('/')) ? targetPath.slice(1) : targetPath
|
||||
return `${baseDocUrl}/${docLanguage}/${targetPath}`
|
||||
}
|
||||
}
|
||||
export default I18NContext
|
||||
27
dify/web/context/mitt-context.tsx
Normal file
27
dify/web/context/mitt-context.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createContext, useContext, useContextSelector } from 'use-context-selector'
|
||||
import { useMitt } from '@/hooks/use-mitt'
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
type ContextValueType = ReturnType<typeof useMitt>
|
||||
export const MittContext = createContext<ContextValueType>({
|
||||
emit: noop,
|
||||
useSubscribe: noop,
|
||||
})
|
||||
|
||||
export const MittProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const mitt = useMitt()
|
||||
|
||||
return (
|
||||
<MittContext.Provider value={mitt}>
|
||||
{children}
|
||||
</MittContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useMittContext = () => {
|
||||
return useContext(MittContext)
|
||||
}
|
||||
|
||||
export function useMittContextSelector<T>(selector: (value: ContextValueType) => T): T {
|
||||
return useContextSelector(MittContext, selector)
|
||||
}
|
||||
186
dify/web/context/modal-context.test.tsx
Normal file
186
dify/web/context/modal-context.test.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import React from 'react'
|
||||
import { act, render, screen, waitFor } from '@testing-library/react'
|
||||
import { ModalContextProvider } from '@/context/modal-context'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { defaultPlan } from '@/app/components/billing/config'
|
||||
|
||||
jest.mock('@/config', () => {
|
||||
const actual = jest.requireActual('@/config')
|
||||
return {
|
||||
...actual,
|
||||
IS_CLOUD_EDITION: true,
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('next/navigation', () => ({
|
||||
useSearchParams: jest.fn(() => new URLSearchParams()),
|
||||
}))
|
||||
|
||||
const mockUseProviderContext = jest.fn()
|
||||
jest.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => mockUseProviderContext(),
|
||||
}))
|
||||
|
||||
const mockUseAppContext = jest.fn()
|
||||
jest.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => mockUseAppContext(),
|
||||
}))
|
||||
|
||||
let latestTriggerEventsModalProps: any = null
|
||||
const triggerEventsLimitModalMock = jest.fn((props: any) => {
|
||||
latestTriggerEventsModalProps = props
|
||||
return (
|
||||
<div data-testid="trigger-limit-modal">
|
||||
<button type="button" onClick={props.onDismiss}>dismiss</button>
|
||||
<button type="button" onClick={props.onUpgrade}>upgrade</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
jest.mock('@/app/components/billing/trigger-events-limit-modal', () => ({
|
||||
__esModule: true,
|
||||
default: (props: any) => triggerEventsLimitModalMock(props),
|
||||
}))
|
||||
|
||||
type DefaultPlanShape = typeof defaultPlan
|
||||
type ResetShape = {
|
||||
apiRateLimit: number | null
|
||||
triggerEvents: number | null
|
||||
}
|
||||
type PlanShape = Omit<DefaultPlanShape, 'reset'> & { reset: ResetShape }
|
||||
type PlanOverrides = Partial<Omit<DefaultPlanShape, 'usage' | 'total' | 'reset'>> & {
|
||||
usage?: Partial<DefaultPlanShape['usage']>
|
||||
total?: Partial<DefaultPlanShape['total']>
|
||||
reset?: Partial<ResetShape>
|
||||
}
|
||||
|
||||
const createPlan = (overrides: PlanOverrides = {}): PlanShape => ({
|
||||
...defaultPlan,
|
||||
...overrides,
|
||||
usage: {
|
||||
...defaultPlan.usage,
|
||||
...overrides.usage,
|
||||
},
|
||||
total: {
|
||||
...defaultPlan.total,
|
||||
...overrides.total,
|
||||
},
|
||||
reset: {
|
||||
...defaultPlan.reset,
|
||||
...overrides.reset,
|
||||
},
|
||||
})
|
||||
|
||||
const renderProvider = () => render(
|
||||
<ModalContextProvider>
|
||||
<div data-testid="modal-context-test-child" />
|
||||
</ModalContextProvider>,
|
||||
)
|
||||
|
||||
describe('ModalContextProvider trigger events limit modal', () => {
|
||||
beforeEach(() => {
|
||||
latestTriggerEventsModalProps = null
|
||||
triggerEventsLimitModalMock.mockClear()
|
||||
mockUseAppContext.mockReset()
|
||||
mockUseProviderContext.mockReset()
|
||||
window.localStorage.clear()
|
||||
mockUseAppContext.mockReturnValue({
|
||||
currentWorkspace: {
|
||||
id: 'workspace-1',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('opens the trigger events limit modal and persists dismissal in localStorage', async () => {
|
||||
const plan = createPlan({
|
||||
type: Plan.professional,
|
||||
usage: { triggerEvents: 3000 },
|
||||
total: { triggerEvents: 3000 },
|
||||
reset: { triggerEvents: 5 },
|
||||
})
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
plan,
|
||||
isFetchedPlan: true,
|
||||
})
|
||||
const setItemSpy = jest.spyOn(Storage.prototype, 'setItem')
|
||||
|
||||
renderProvider()
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal')).toBeInTheDocument())
|
||||
expect(latestTriggerEventsModalProps).toMatchObject({
|
||||
usage: 3000,
|
||||
total: 3000,
|
||||
resetInDays: 5,
|
||||
planType: Plan.professional,
|
||||
})
|
||||
|
||||
act(() => {
|
||||
latestTriggerEventsModalProps.onDismiss()
|
||||
})
|
||||
|
||||
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
|
||||
const [key, value] = setItemSpy.mock.calls[0]
|
||||
expect(key).toContain('trigger-events-limit-dismissed-workspace-1-professional-3000-')
|
||||
expect(value).toBe('1')
|
||||
})
|
||||
|
||||
it('relies on the in-memory guard when localStorage reads throw', async () => {
|
||||
const plan = createPlan({
|
||||
type: Plan.professional,
|
||||
usage: { triggerEvents: 200 },
|
||||
total: { triggerEvents: 200 },
|
||||
reset: { triggerEvents: 3 },
|
||||
})
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
plan,
|
||||
isFetchedPlan: true,
|
||||
})
|
||||
jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
|
||||
throw new Error('Storage disabled')
|
||||
})
|
||||
const setItemSpy = jest.spyOn(Storage.prototype, 'setItem')
|
||||
|
||||
renderProvider()
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal')).toBeInTheDocument())
|
||||
|
||||
act(() => {
|
||||
latestTriggerEventsModalProps.onDismiss()
|
||||
})
|
||||
|
||||
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
|
||||
expect(setItemSpy).not.toHaveBeenCalled()
|
||||
await waitFor(() => expect(triggerEventsLimitModalMock).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
|
||||
it('falls back to the in-memory guard when localStorage.setItem fails', async () => {
|
||||
const plan = createPlan({
|
||||
type: Plan.professional,
|
||||
usage: { triggerEvents: 120 },
|
||||
total: { triggerEvents: 120 },
|
||||
reset: { triggerEvents: 2 },
|
||||
})
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
plan,
|
||||
isFetchedPlan: true,
|
||||
})
|
||||
jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
|
||||
throw new Error('Quota exceeded')
|
||||
})
|
||||
|
||||
renderProvider()
|
||||
|
||||
await waitFor(() => expect(screen.getByTestId('trigger-limit-modal')).toBeInTheDocument())
|
||||
|
||||
act(() => {
|
||||
latestTriggerEventsModalProps.onDismiss()
|
||||
})
|
||||
|
||||
await waitFor(() => expect(screen.queryByTestId('trigger-limit-modal')).not.toBeInTheDocument())
|
||||
await waitFor(() => expect(triggerEventsLimitModalMock).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
})
|
||||
506
dify/web/context/modal-context.tsx
Normal file
506
dify/web/context/modal-context.tsx
Normal file
@@ -0,0 +1,506 @@
|
||||
'use client'
|
||||
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { createContext, useContext, useContextSelector } from 'use-context-selector'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import type {
|
||||
ConfigurationMethodEnum,
|
||||
Credential,
|
||||
CustomConfigurationModelFixedFields,
|
||||
CustomModel,
|
||||
ModelProvider,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import {
|
||||
EDUCATION_PRICING_SHOW_ACTION,
|
||||
EDUCATION_VERIFYING_LOCALSTORAGE_ITEM,
|
||||
} from '@/app/education-apply/constants'
|
||||
import type { AccountSettingTab } from '@/app/components/header/account-setting/constants'
|
||||
import {
|
||||
ACCOUNT_SETTING_MODAL_ACTION,
|
||||
DEFAULT_ACCOUNT_SETTING_TAB,
|
||||
isValidAccountSettingTab,
|
||||
} from '@/app/components/header/account-setting/constants'
|
||||
import type { ModerationConfig, PromptVariable } from '@/models/debug'
|
||||
import type {
|
||||
ApiBasedExtension,
|
||||
ExternalDataTool,
|
||||
} from '@/models/common'
|
||||
import type { CreateExternalAPIReq } from '@/app/components/datasets/external-api/declarations'
|
||||
import type { ModelLoadBalancingModalProps } from '@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'
|
||||
import type { OpeningStatement } from '@/app/components/base/features/types'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import type { UpdatePluginPayload } from '@/app/components/plugins/types'
|
||||
import { removeSpecificQueryParam } from '@/utils'
|
||||
import { noop } from 'lodash-es'
|
||||
import dynamic from 'next/dynamic'
|
||||
import type { ExpireNoticeModalPayloadProps } from '@/app/education-apply/expire-notice-modal'
|
||||
import type { ModelModalModeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import {
|
||||
type TriggerEventsLimitModalPayload,
|
||||
useTriggerEventsLimitModal,
|
||||
} from './hooks/use-trigger-events-limit-modal'
|
||||
|
||||
const AccountSetting = dynamic(() => import('@/app/components/header/account-setting'), {
|
||||
ssr: false,
|
||||
})
|
||||
const ApiBasedExtensionModal = dynamic(() => import('@/app/components/header/account-setting/api-based-extension-page/modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const ModerationSettingModal = dynamic(() => import('@/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const ExternalDataToolModal = dynamic(() => import('@/app/components/app/configuration/tools/external-data-tool-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const Pricing = dynamic(() => import('@/app/components/billing/pricing'), {
|
||||
ssr: false,
|
||||
})
|
||||
const AnnotationFullModal = dynamic(() => import('@/app/components/billing/annotation-full/modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const ModelModal = dynamic(() => import('@/app/components/header/account-setting/model-provider-page/model-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const ExternalAPIModal = dynamic(() => import('@/app/components/datasets/external-api/external-api-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const ModelLoadBalancingModal = dynamic(() => import('@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const OpeningSettingModal = dynamic(() => import('@/app/components/base/features/new-feature-panel/conversation-opener/modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const UpdatePlugin = dynamic(() => import('@/app/components/plugins/update-plugin'), {
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
const ExpireNoticeModal = dynamic(() => import('@/app/education-apply/expire-notice-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
const TriggerEventsLimitModal = dynamic(() => import('@/app/components/billing/trigger-events-limit-modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
export type ModalState<T> = {
|
||||
payload: T
|
||||
onCancelCallback?: () => void
|
||||
onSaveCallback?: (newPayload?: T, formValues?: Record<string, any>) => void
|
||||
onRemoveCallback?: (newPayload?: T, formValues?: Record<string, any>) => void
|
||||
onEditCallback?: (newPayload: T) => void
|
||||
onValidateBeforeSaveCallback?: (newPayload: T) => boolean
|
||||
isEditMode?: boolean
|
||||
datasetBindings?: { id: string; name: string }[]
|
||||
}
|
||||
|
||||
export type ModelModalType = {
|
||||
currentProvider: ModelProvider
|
||||
currentConfigurationMethod: ConfigurationMethodEnum
|
||||
currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields
|
||||
isModelCredential?: boolean
|
||||
credential?: Credential
|
||||
model?: CustomModel
|
||||
mode?: ModelModalModeEnum
|
||||
}
|
||||
|
||||
export type ModalContextState = {
|
||||
setShowAccountSettingModal: Dispatch<SetStateAction<ModalState<AccountSettingTab> | null>>
|
||||
setShowApiBasedExtensionModal: Dispatch<SetStateAction<ModalState<ApiBasedExtension> | null>>
|
||||
setShowModerationSettingModal: Dispatch<SetStateAction<ModalState<ModerationConfig> | null>>
|
||||
setShowExternalDataToolModal: Dispatch<SetStateAction<ModalState<ExternalDataTool> | null>>
|
||||
setShowPricingModal: () => void
|
||||
setShowAnnotationFullModal: () => void
|
||||
setShowModelModal: Dispatch<SetStateAction<ModalState<ModelModalType> | null>>
|
||||
setShowExternalKnowledgeAPIModal: Dispatch<SetStateAction<ModalState<CreateExternalAPIReq> | null>>
|
||||
setShowModelLoadBalancingModal: Dispatch<SetStateAction<ModelLoadBalancingModalProps | null>>
|
||||
setShowOpeningModal: Dispatch<SetStateAction<ModalState<OpeningStatement & {
|
||||
promptVariables?: PromptVariable[]
|
||||
workflowVariables?: InputVar[]
|
||||
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
|
||||
}> | null>>
|
||||
setShowUpdatePluginModal: Dispatch<SetStateAction<ModalState<UpdatePluginPayload> | null>>
|
||||
setShowEducationExpireNoticeModal: Dispatch<SetStateAction<ModalState<ExpireNoticeModalPayloadProps> | null>>
|
||||
setShowTriggerEventsLimitModal: Dispatch<SetStateAction<ModalState<TriggerEventsLimitModalPayload> | null>>
|
||||
}
|
||||
const PRICING_MODAL_QUERY_PARAM = 'pricing'
|
||||
const PRICING_MODAL_QUERY_VALUE = 'open'
|
||||
|
||||
const ModalContext = createContext<ModalContextState>({
|
||||
setShowAccountSettingModal: noop,
|
||||
setShowApiBasedExtensionModal: noop,
|
||||
setShowModerationSettingModal: noop,
|
||||
setShowExternalDataToolModal: noop,
|
||||
setShowPricingModal: noop,
|
||||
setShowAnnotationFullModal: noop,
|
||||
setShowModelModal: noop,
|
||||
setShowExternalKnowledgeAPIModal: noop,
|
||||
setShowModelLoadBalancingModal: noop,
|
||||
setShowOpeningModal: noop,
|
||||
setShowUpdatePluginModal: noop,
|
||||
setShowEducationExpireNoticeModal: noop,
|
||||
setShowTriggerEventsLimitModal: noop,
|
||||
})
|
||||
|
||||
export const useModalContext = () => useContext(ModalContext)
|
||||
|
||||
// Adding a dangling comma to avoid the generic parsing issue in tsx, see:
|
||||
// https://github.com/microsoft/TypeScript/issues/15713
|
||||
export const useModalContextSelector = <T,>(selector: (state: ModalContextState) => T): T =>
|
||||
useContextSelector(ModalContext, selector)
|
||||
|
||||
type ModalContextProviderProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
export const ModalContextProvider = ({
|
||||
children,
|
||||
}: ModalContextProviderProps) => {
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
const [showAccountSettingModal, setShowAccountSettingModal] = useState<ModalState<AccountSettingTab> | null>(() => {
|
||||
if (searchParams.get('action') === ACCOUNT_SETTING_MODAL_ACTION) {
|
||||
const tabParam = searchParams.get('tab')
|
||||
const tab = isValidAccountSettingTab(tabParam) ? tabParam : DEFAULT_ACCOUNT_SETTING_TAB
|
||||
return { payload: tab }
|
||||
}
|
||||
return null
|
||||
})
|
||||
const [showApiBasedExtensionModal, setShowApiBasedExtensionModal] = useState<ModalState<ApiBasedExtension> | null>(null)
|
||||
const [showModerationSettingModal, setShowModerationSettingModal] = useState<ModalState<ModerationConfig> | null>(null)
|
||||
const [showExternalDataToolModal, setShowExternalDataToolModal] = useState<ModalState<ExternalDataTool> | null>(null)
|
||||
const [showModelModal, setShowModelModal] = useState<ModalState<ModelModalType> | null>(null)
|
||||
const [showExternalKnowledgeAPIModal, setShowExternalKnowledgeAPIModal] = useState<ModalState<CreateExternalAPIReq> | null>(null)
|
||||
const [showModelLoadBalancingModal, setShowModelLoadBalancingModal] = useState<ModelLoadBalancingModalProps | null>(null)
|
||||
const [showOpeningModal, setShowOpeningModal] = useState<ModalState<OpeningStatement & {
|
||||
promptVariables?: PromptVariable[]
|
||||
workflowVariables?: InputVar[]
|
||||
onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
|
||||
}> | null>(null)
|
||||
const [showUpdatePluginModal, setShowUpdatePluginModal] = useState<ModalState<UpdatePluginPayload> | null>(null)
|
||||
const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] = useState<ModalState<ExpireNoticeModalPayloadProps> | null>(null)
|
||||
const { currentWorkspace } = useAppContext()
|
||||
|
||||
const [showPricingModal, setShowPricingModal] = useState(
|
||||
searchParams.get(PRICING_MODAL_QUERY_PARAM) === PRICING_MODAL_QUERY_VALUE,
|
||||
)
|
||||
const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false)
|
||||
const handleCancelAccountSettingModal = () => {
|
||||
const educationVerifying = localStorage.getItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
|
||||
if (educationVerifying === 'yes')
|
||||
localStorage.removeItem(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM)
|
||||
|
||||
removeSpecificQueryParam('action')
|
||||
removeSpecificQueryParam('tab')
|
||||
setShowAccountSettingModal(null)
|
||||
if (showAccountSettingModal?.onCancelCallback)
|
||||
showAccountSettingModal?.onCancelCallback()
|
||||
}
|
||||
|
||||
const handleAccountSettingTabChange = useCallback((tab: AccountSettingTab) => {
|
||||
setShowAccountSettingModal((prev) => {
|
||||
if (!prev)
|
||||
return { payload: tab }
|
||||
if (prev.payload === tab)
|
||||
return prev
|
||||
return { ...prev, payload: tab }
|
||||
})
|
||||
}, [setShowAccountSettingModal])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined')
|
||||
return
|
||||
const url = new URL(window.location.href)
|
||||
if (!showAccountSettingModal?.payload) {
|
||||
if (url.searchParams.get('action') !== ACCOUNT_SETTING_MODAL_ACTION)
|
||||
return
|
||||
url.searchParams.delete('action')
|
||||
url.searchParams.delete('tab')
|
||||
window.history.replaceState(null, '', url.toString())
|
||||
return
|
||||
}
|
||||
url.searchParams.set('action', ACCOUNT_SETTING_MODAL_ACTION)
|
||||
url.searchParams.set('tab', showAccountSettingModal.payload)
|
||||
window.history.replaceState(null, '', url.toString())
|
||||
}, [showAccountSettingModal])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined')
|
||||
return
|
||||
const url = new URL(window.location.href)
|
||||
if (showPricingModal) {
|
||||
url.searchParams.set(PRICING_MODAL_QUERY_PARAM, PRICING_MODAL_QUERY_VALUE)
|
||||
}
|
||||
else {
|
||||
url.searchParams.delete(PRICING_MODAL_QUERY_PARAM)
|
||||
if (url.searchParams.get('action') === EDUCATION_PRICING_SHOW_ACTION)
|
||||
url.searchParams.delete('action')
|
||||
}
|
||||
window.history.replaceState(null, '', url.toString())
|
||||
}, [showPricingModal])
|
||||
|
||||
const { plan, isFetchedPlan } = useProviderContext()
|
||||
const {
|
||||
showTriggerEventsLimitModal,
|
||||
setShowTriggerEventsLimitModal,
|
||||
persistTriggerEventsLimitModalDismiss,
|
||||
} = useTriggerEventsLimitModal({
|
||||
plan,
|
||||
isFetchedPlan,
|
||||
currentWorkspaceId: currentWorkspace?.id,
|
||||
})
|
||||
|
||||
const handleCancelModerationSettingModal = () => {
|
||||
setShowModerationSettingModal(null)
|
||||
if (showModerationSettingModal?.onCancelCallback)
|
||||
showModerationSettingModal.onCancelCallback()
|
||||
}
|
||||
|
||||
const handleCancelExternalDataToolModal = () => {
|
||||
setShowExternalDataToolModal(null)
|
||||
if (showExternalDataToolModal?.onCancelCallback)
|
||||
showExternalDataToolModal.onCancelCallback()
|
||||
}
|
||||
|
||||
const handleCancelModelModal = useCallback(() => {
|
||||
setShowModelModal(null)
|
||||
if (showModelModal?.onCancelCallback)
|
||||
showModelModal.onCancelCallback()
|
||||
}, [showModelModal])
|
||||
|
||||
const handleSaveModelModal = useCallback((formValues?: Record<string, any>) => {
|
||||
if (showModelModal?.onSaveCallback)
|
||||
showModelModal.onSaveCallback(showModelModal.payload, formValues)
|
||||
setShowModelModal(null)
|
||||
}, [showModelModal])
|
||||
|
||||
const handleRemoveModelModal = useCallback((formValues?: Record<string, any>) => {
|
||||
if (showModelModal?.onRemoveCallback)
|
||||
showModelModal.onRemoveCallback(showModelModal.payload, formValues)
|
||||
setShowModelModal(null)
|
||||
}, [showModelModal])
|
||||
|
||||
const handleCancelExternalApiModal = useCallback(() => {
|
||||
setShowExternalKnowledgeAPIModal(null)
|
||||
if (showExternalKnowledgeAPIModal?.onCancelCallback)
|
||||
showExternalKnowledgeAPIModal.onCancelCallback()
|
||||
}, [showExternalKnowledgeAPIModal])
|
||||
|
||||
const handleSaveExternalApiModal = useCallback(async (updatedFormValue: CreateExternalAPIReq) => {
|
||||
if (showExternalKnowledgeAPIModal?.onSaveCallback)
|
||||
showExternalKnowledgeAPIModal.onSaveCallback(updatedFormValue)
|
||||
setShowExternalKnowledgeAPIModal(null)
|
||||
}, [showExternalKnowledgeAPIModal])
|
||||
|
||||
const handleEditExternalApiModal = useCallback(async (updatedFormValue: CreateExternalAPIReq) => {
|
||||
if (showExternalKnowledgeAPIModal?.onEditCallback)
|
||||
showExternalKnowledgeAPIModal.onEditCallback(updatedFormValue)
|
||||
setShowExternalKnowledgeAPIModal(null)
|
||||
}, [showExternalKnowledgeAPIModal])
|
||||
|
||||
const handleCancelOpeningModal = useCallback(() => {
|
||||
setShowOpeningModal(null)
|
||||
if (showOpeningModal?.onCancelCallback)
|
||||
showOpeningModal.onCancelCallback()
|
||||
}, [showOpeningModal])
|
||||
|
||||
const handleSaveApiBasedExtension = (newApiBasedExtension: ApiBasedExtension) => {
|
||||
if (showApiBasedExtensionModal?.onSaveCallback)
|
||||
showApiBasedExtensionModal.onSaveCallback(newApiBasedExtension)
|
||||
setShowApiBasedExtensionModal(null)
|
||||
}
|
||||
|
||||
const handleSaveModeration = (newModerationConfig: ModerationConfig) => {
|
||||
if (showModerationSettingModal?.onSaveCallback)
|
||||
showModerationSettingModal.onSaveCallback(newModerationConfig)
|
||||
setShowModerationSettingModal(null)
|
||||
}
|
||||
|
||||
const handleSaveExternalDataTool = (newExternalDataTool: ExternalDataTool) => {
|
||||
if (showExternalDataToolModal?.onSaveCallback)
|
||||
showExternalDataToolModal.onSaveCallback(newExternalDataTool)
|
||||
setShowExternalDataToolModal(null)
|
||||
}
|
||||
|
||||
const handleValidateBeforeSaveExternalDataTool = (newExternalDataTool: ExternalDataTool) => {
|
||||
if (showExternalDataToolModal?.onValidateBeforeSaveCallback)
|
||||
return showExternalDataToolModal?.onValidateBeforeSaveCallback(newExternalDataTool)
|
||||
return true
|
||||
}
|
||||
|
||||
const handleSaveOpeningModal = (newOpening: OpeningStatement) => {
|
||||
if (showOpeningModal?.onSaveCallback)
|
||||
showOpeningModal.onSaveCallback(newOpening)
|
||||
setShowOpeningModal(null)
|
||||
}
|
||||
|
||||
const handleShowPricingModal = useCallback(() => {
|
||||
setShowPricingModal(true)
|
||||
}, [])
|
||||
|
||||
const handleCancelPricingModal = useCallback(() => {
|
||||
setShowPricingModal(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ModalContext.Provider value={{
|
||||
setShowAccountSettingModal,
|
||||
setShowApiBasedExtensionModal,
|
||||
setShowModerationSettingModal,
|
||||
setShowExternalDataToolModal,
|
||||
setShowPricingModal: handleShowPricingModal,
|
||||
setShowAnnotationFullModal: () => setShowAnnotationFullModal(true),
|
||||
setShowModelModal,
|
||||
setShowExternalKnowledgeAPIModal,
|
||||
setShowModelLoadBalancingModal,
|
||||
setShowOpeningModal,
|
||||
setShowUpdatePluginModal,
|
||||
setShowEducationExpireNoticeModal,
|
||||
setShowTriggerEventsLimitModal,
|
||||
}}>
|
||||
<>
|
||||
{children}
|
||||
{
|
||||
!!showAccountSettingModal && (
|
||||
<AccountSetting
|
||||
activeTab={showAccountSettingModal.payload}
|
||||
onCancel={handleCancelAccountSettingModal}
|
||||
onTabChange={handleAccountSettingTabChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
!!showApiBasedExtensionModal && (
|
||||
<ApiBasedExtensionModal
|
||||
data={showApiBasedExtensionModal.payload}
|
||||
onCancel={() => setShowApiBasedExtensionModal(null)}
|
||||
onSave={handleSaveApiBasedExtension}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!showModerationSettingModal && (
|
||||
<ModerationSettingModal
|
||||
data={showModerationSettingModal.payload}
|
||||
onCancel={handleCancelModerationSettingModal}
|
||||
onSave={handleSaveModeration}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!showExternalDataToolModal && (
|
||||
<ExternalDataToolModal
|
||||
data={showExternalDataToolModal.payload}
|
||||
onCancel={handleCancelExternalDataToolModal}
|
||||
onSave={handleSaveExternalDataTool}
|
||||
onValidateBeforeSave={handleValidateBeforeSaveExternalDataTool}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
!!showPricingModal && (
|
||||
<Pricing onCancel={handleCancelPricingModal} />
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
showAnnotationFullModal && (
|
||||
<AnnotationFullModal
|
||||
show={showAnnotationFullModal}
|
||||
onHide={() => setShowAnnotationFullModal(false)} />
|
||||
)
|
||||
}
|
||||
{
|
||||
!!showModelModal && (
|
||||
<ModelModal
|
||||
provider={showModelModal.payload.currentProvider}
|
||||
configurateMethod={showModelModal.payload.currentConfigurationMethod}
|
||||
currentCustomConfigurationModelFixedFields={showModelModal.payload.currentCustomConfigurationModelFixedFields}
|
||||
isModelCredential={showModelModal.payload.isModelCredential}
|
||||
credential={showModelModal.payload.credential}
|
||||
model={showModelModal.payload.model}
|
||||
mode={showModelModal.payload.mode}
|
||||
onCancel={handleCancelModelModal}
|
||||
onSave={handleSaveModelModal}
|
||||
onRemove={handleRemoveModelModal}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!showExternalKnowledgeAPIModal && (
|
||||
<ExternalAPIModal
|
||||
data={showExternalKnowledgeAPIModal.payload}
|
||||
datasetBindings={showExternalKnowledgeAPIModal.datasetBindings ?? []}
|
||||
onSave={handleSaveExternalApiModal}
|
||||
onCancel={handleCancelExternalApiModal}
|
||||
onEdit={handleEditExternalApiModal}
|
||||
isEditMode={showExternalKnowledgeAPIModal.isEditMode ?? false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
Boolean(showModelLoadBalancingModal) && (
|
||||
<ModelLoadBalancingModal {...showModelLoadBalancingModal!} />
|
||||
)
|
||||
}
|
||||
{showOpeningModal && (
|
||||
<OpeningSettingModal
|
||||
data={showOpeningModal.payload}
|
||||
onSave={handleSaveOpeningModal}
|
||||
onCancel={handleCancelOpeningModal}
|
||||
promptVariables={showOpeningModal.payload.promptVariables}
|
||||
workflowVariables={showOpeningModal.payload.workflowVariables}
|
||||
onAutoAddPromptVariable={showOpeningModal.payload.onAutoAddPromptVariable}
|
||||
/>
|
||||
)}
|
||||
|
||||
{
|
||||
!!showUpdatePluginModal && (
|
||||
<UpdatePlugin
|
||||
{...showUpdatePluginModal.payload}
|
||||
onCancel={() => {
|
||||
setShowUpdatePluginModal(null)
|
||||
showUpdatePluginModal.onCancelCallback?.()
|
||||
}}
|
||||
onSave={() => {
|
||||
setShowUpdatePluginModal(null)
|
||||
showUpdatePluginModal.onSaveCallback?.({} as any)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!showEducationExpireNoticeModal && (
|
||||
<ExpireNoticeModal
|
||||
{...showEducationExpireNoticeModal.payload}
|
||||
onClose={() => setShowEducationExpireNoticeModal(null)}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
!!showTriggerEventsLimitModal && (
|
||||
<TriggerEventsLimitModal
|
||||
show
|
||||
usage={showTriggerEventsLimitModal.payload.usage}
|
||||
total={showTriggerEventsLimitModal.payload.total}
|
||||
planType={showTriggerEventsLimitModal.payload.planType}
|
||||
resetInDays={showTriggerEventsLimitModal.payload.resetInDays}
|
||||
onDismiss={() => {
|
||||
persistTriggerEventsLimitModalDismiss()
|
||||
setShowTriggerEventsLimitModal(null)
|
||||
}}
|
||||
onUpgrade={() => {
|
||||
persistTriggerEventsLimitModalDismiss()
|
||||
setShowTriggerEventsLimitModal(null)
|
||||
handleShowPricingModal()
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</ModalContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalContext
|
||||
253
dify/web/context/provider-context.tsx
Normal file
253
dify/web/context/provider-context.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useContextSelector } from 'use-context-selector'
|
||||
import useSWR from 'swr'
|
||||
import { useEffect, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
fetchModelList,
|
||||
fetchModelProviders,
|
||||
fetchSupportRetrievalMethods,
|
||||
} from '@/service/common'
|
||||
import {
|
||||
CurrentSystemQuotaTypeEnum,
|
||||
ModelStatusEnum,
|
||||
ModelTypeEnum,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { Model, ModelProvider } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { RETRIEVE_METHOD } from '@/types/app'
|
||||
import type { Plan, UsageResetInfo } from '@/app/components/billing/type'
|
||||
import type { UsagePlanInfo } from '@/app/components/billing/type'
|
||||
import { fetchCurrentPlanInfo } from '@/service/billing'
|
||||
import { parseCurrentPlan } from '@/app/components/billing/utils'
|
||||
import { defaultPlan } from '@/app/components/billing/config'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import {
|
||||
useEducationStatus,
|
||||
} from '@/service/use-education'
|
||||
import { noop } from 'lodash-es'
|
||||
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
|
||||
import { ZENDESK_FIELD_IDS } from '@/config'
|
||||
|
||||
type ProviderContextState = {
|
||||
modelProviders: ModelProvider[]
|
||||
refreshModelProviders: () => void
|
||||
textGenerationModelList: Model[]
|
||||
supportRetrievalMethods: RETRIEVE_METHOD[]
|
||||
isAPIKeySet: boolean
|
||||
plan: {
|
||||
type: Plan
|
||||
usage: UsagePlanInfo
|
||||
total: UsagePlanInfo
|
||||
reset: UsageResetInfo
|
||||
}
|
||||
isFetchedPlan: boolean
|
||||
enableBilling: boolean
|
||||
onPlanInfoChanged: () => void
|
||||
enableReplaceWebAppLogo: boolean
|
||||
modelLoadBalancingEnabled: boolean
|
||||
datasetOperatorEnabled: boolean
|
||||
enableEducationPlan: boolean
|
||||
isEducationWorkspace: boolean
|
||||
isEducationAccount: boolean
|
||||
allowRefreshEducationVerify: boolean
|
||||
educationAccountExpireAt: number | null
|
||||
isLoadingEducationAccountInfo: boolean
|
||||
isFetchingEducationAccountInfo: boolean
|
||||
webappCopyrightEnabled: boolean
|
||||
licenseLimit: {
|
||||
workspace_members: {
|
||||
size: number
|
||||
limit: number
|
||||
}
|
||||
},
|
||||
refreshLicenseLimit: () => void
|
||||
isAllowTransferWorkspace: boolean
|
||||
isAllowPublishAsCustomKnowledgePipelineTemplate: boolean
|
||||
}
|
||||
const ProviderContext = createContext<ProviderContextState>({
|
||||
modelProviders: [],
|
||||
refreshModelProviders: noop,
|
||||
textGenerationModelList: [],
|
||||
supportRetrievalMethods: [],
|
||||
isAPIKeySet: true,
|
||||
plan: defaultPlan,
|
||||
isFetchedPlan: false,
|
||||
enableBilling: false,
|
||||
onPlanInfoChanged: noop,
|
||||
enableReplaceWebAppLogo: false,
|
||||
modelLoadBalancingEnabled: false,
|
||||
datasetOperatorEnabled: false,
|
||||
enableEducationPlan: false,
|
||||
isEducationWorkspace: false,
|
||||
isEducationAccount: false,
|
||||
allowRefreshEducationVerify: false,
|
||||
educationAccountExpireAt: null,
|
||||
isLoadingEducationAccountInfo: false,
|
||||
isFetchingEducationAccountInfo: false,
|
||||
webappCopyrightEnabled: false,
|
||||
licenseLimit: {
|
||||
workspace_members: {
|
||||
size: 0,
|
||||
limit: 0,
|
||||
},
|
||||
},
|
||||
refreshLicenseLimit: noop,
|
||||
isAllowTransferWorkspace: false,
|
||||
isAllowPublishAsCustomKnowledgePipelineTemplate: false,
|
||||
})
|
||||
|
||||
export const useProviderContext = () => useContext(ProviderContext)
|
||||
|
||||
// Adding a dangling comma to avoid the generic parsing issue in tsx, see:
|
||||
// https://github.com/microsoft/TypeScript/issues/15713
|
||||
export const useProviderContextSelector = <T,>(selector: (state: ProviderContextState) => T): T =>
|
||||
useContextSelector(ProviderContext, selector)
|
||||
|
||||
type ProviderContextProviderProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
export const ProviderContextProvider = ({
|
||||
children,
|
||||
}: ProviderContextProviderProps) => {
|
||||
const { data: providersData, mutate: refreshModelProviders } = useSWR('/workspaces/current/model-providers', fetchModelProviders)
|
||||
const fetchModelListUrlPrefix = '/workspaces/current/models/model-types/'
|
||||
const { data: textGenerationModelList } = useSWR(`${fetchModelListUrlPrefix}${ModelTypeEnum.textGeneration}`, fetchModelList)
|
||||
const { data: supportRetrievalMethods } = useSWR('/datasets/retrieval-setting', fetchSupportRetrievalMethods)
|
||||
|
||||
const [plan, setPlan] = useState(defaultPlan)
|
||||
const [isFetchedPlan, setIsFetchedPlan] = useState(false)
|
||||
const [enableBilling, setEnableBilling] = useState(true)
|
||||
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)
|
||||
const { data: educationAccountInfo, isLoading: isLoadingEducationAccountInfo, isFetching: isFetchingEducationAccountInfo } = useEducationStatus(!enableEducationPlan)
|
||||
const [isAllowTransferWorkspace, setIsAllowTransferWorkspace] = useState(false)
|
||||
const [isAllowPublishAsCustomKnowledgePipelineTemplate, setIsAllowPublishAsCustomKnowledgePipelineTemplate] = useState(false)
|
||||
|
||||
const fetchPlan = async () => {
|
||||
try {
|
||||
const data = await fetchCurrentPlanInfo()
|
||||
if (!data) {
|
||||
console.error('Failed to fetch plan info: data is undefined')
|
||||
return
|
||||
}
|
||||
|
||||
// set default value to avoid undefined error
|
||||
setEnableBilling(data.billing?.enabled ?? false)
|
||||
setEnableEducationPlan(data.education?.enabled ?? false)
|
||||
setIsEducationWorkspace(data.education?.activated ?? false)
|
||||
setEnableReplaceWebAppLogo(data.can_replace_logo ?? false)
|
||||
|
||||
if (data.billing?.enabled) {
|
||||
setPlan(parseCurrentPlan(data) as any)
|
||||
setIsFetchedPlan(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 })
|
||||
if (data.is_allow_transfer_workspace)
|
||||
setIsAllowTransferWorkspace(data.is_allow_transfer_workspace)
|
||||
if (data.knowledge_pipeline?.publish_enabled)
|
||||
setIsAllowPublishAsCustomKnowledgePipelineTemplate(data.knowledge_pipeline?.publish_enabled)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Failed to fetch plan info:', error)
|
||||
// set default value to avoid undefined error
|
||||
setEnableBilling(false)
|
||||
setEnableEducationPlan(false)
|
||||
setIsEducationWorkspace(false)
|
||||
setEnableReplaceWebAppLogo(false)
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
fetchPlan()
|
||||
}, [])
|
||||
|
||||
// #region Zendesk conversation fields
|
||||
useEffect(() => {
|
||||
if (ZENDESK_FIELD_IDS.PLAN && plan.type) {
|
||||
setZendeskConversationFields([{
|
||||
id: ZENDESK_FIELD_IDS.PLAN,
|
||||
value: `${plan.type}-plan`,
|
||||
}])
|
||||
}
|
||||
}, [plan.type])
|
||||
// #endregion Zendesk conversation fields
|
||||
|
||||
const { t } = useTranslation()
|
||||
useEffect(() => {
|
||||
if (localStorage.getItem('anthropic_quota_notice') === 'true')
|
||||
return
|
||||
|
||||
if (dayjs().isAfter(dayjs('2025-03-17')))
|
||||
return
|
||||
|
||||
if (providersData?.data && providersData.data.length > 0) {
|
||||
const anthropic = providersData.data.find(provider => provider.provider === 'anthropic')
|
||||
if (anthropic && anthropic.system_configuration.current_quota_type === CurrentSystemQuotaTypeEnum.trial) {
|
||||
const quota = anthropic.system_configuration.quota_configurations.find(item => item.quota_type === anthropic.system_configuration.current_quota_type)
|
||||
if (quota && quota.is_valid && quota.quota_used < quota.quota_limit) {
|
||||
Toast.notify({
|
||||
type: 'info',
|
||||
message: t('common.provider.anthropicHosted.trialQuotaTip'),
|
||||
duration: 60000,
|
||||
onClose: () => {
|
||||
localStorage.setItem('anthropic_quota_notice', 'true')
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [providersData, t])
|
||||
|
||||
return (
|
||||
<ProviderContext.Provider value={{
|
||||
modelProviders: providersData?.data || [],
|
||||
refreshModelProviders,
|
||||
textGenerationModelList: textGenerationModelList?.data || [],
|
||||
isAPIKeySet: !!textGenerationModelList?.data.some(model => model.status === ModelStatusEnum.active),
|
||||
supportRetrievalMethods: supportRetrievalMethods?.retrieval_method || [],
|
||||
plan,
|
||||
isFetchedPlan,
|
||||
enableBilling,
|
||||
onPlanInfoChanged: fetchPlan,
|
||||
enableReplaceWebAppLogo,
|
||||
modelLoadBalancingEnabled,
|
||||
datasetOperatorEnabled,
|
||||
enableEducationPlan,
|
||||
isEducationWorkspace,
|
||||
isEducationAccount: educationAccountInfo?.is_student || false,
|
||||
allowRefreshEducationVerify: educationAccountInfo?.allow_refresh || false,
|
||||
educationAccountExpireAt: educationAccountInfo?.expire_at || null,
|
||||
isLoadingEducationAccountInfo,
|
||||
isFetchingEducationAccountInfo,
|
||||
webappCopyrightEnabled,
|
||||
licenseLimit,
|
||||
refreshLicenseLimit: fetchPlan,
|
||||
isAllowTransferWorkspace,
|
||||
isAllowPublishAsCustomKnowledgePipelineTemplate,
|
||||
}}>
|
||||
{children}
|
||||
</ProviderContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProviderContext
|
||||
23
dify/web/context/query-client.tsx
Normal file
23
dify/web/context/query-client.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
|
||||
|
||||
const STALE_TIME = 1000 * 60 * 30 // 30 minutes
|
||||
|
||||
const client = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: STALE_TIME,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const TanstackQueryInitializer: FC<PropsWithChildren> = (props) => {
|
||||
const { children } = props
|
||||
return <QueryClientProvider client={client}>
|
||||
{children}
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
}
|
||||
125
dify/web/context/web-app-context.tsx
Normal file
125
dify/web/context/web-app-context.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
'use client'
|
||||
|
||||
import type { ChatConfig } from '@/app/components/base/chat/types'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import type { AppData, AppMeta } from '@/models/share'
|
||||
import { useGetWebAppAccessModeByCode } from '@/service/use-share'
|
||||
import { usePathname, useSearchParams } from 'next/navigation'
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { create } from 'zustand'
|
||||
import { getProcessedSystemVariablesFromUrlParams } from '@/app/components/base/chat/utils'
|
||||
import { useGlobalPublicStore } from './global-public-context'
|
||||
|
||||
type WebAppStore = {
|
||||
shareCode: string | null
|
||||
updateShareCode: (shareCode: string | null) => void
|
||||
appInfo: AppData | null
|
||||
updateAppInfo: (appInfo: AppData | null) => void
|
||||
appParams: ChatConfig | null
|
||||
updateAppParams: (appParams: ChatConfig | null) => void
|
||||
webAppAccessMode: AccessMode
|
||||
updateWebAppAccessMode: (accessMode: AccessMode) => void
|
||||
appMeta: AppMeta | null
|
||||
updateWebAppMeta: (appMeta: AppMeta | null) => void
|
||||
userCanAccessApp: boolean
|
||||
updateUserCanAccessApp: (canAccess: boolean) => void
|
||||
embeddedUserId: string | null
|
||||
updateEmbeddedUserId: (userId: string | null) => void
|
||||
embeddedConversationId: string | null
|
||||
updateEmbeddedConversationId: (conversationId: string | null) => void
|
||||
}
|
||||
|
||||
export const useWebAppStore = create<WebAppStore>(set => ({
|
||||
shareCode: null,
|
||||
updateShareCode: (shareCode: string | null) => set(() => ({ shareCode })),
|
||||
appInfo: null,
|
||||
updateAppInfo: (appInfo: AppData | null) => set(() => ({ appInfo })),
|
||||
appParams: null,
|
||||
updateAppParams: (appParams: ChatConfig | null) => set(() => ({ appParams })),
|
||||
webAppAccessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
|
||||
updateWebAppAccessMode: (accessMode: AccessMode) => set(() => ({ webAppAccessMode: accessMode })),
|
||||
appMeta: null,
|
||||
updateWebAppMeta: (appMeta: AppMeta | null) => set(() => ({ appMeta })),
|
||||
userCanAccessApp: false,
|
||||
updateUserCanAccessApp: (canAccess: boolean) => set(() => ({ userCanAccessApp: canAccess })),
|
||||
embeddedUserId: null,
|
||||
updateEmbeddedUserId: (userId: string | null) => set(() => ({ embeddedUserId: userId })),
|
||||
embeddedConversationId: null,
|
||||
updateEmbeddedConversationId: (conversationId: string | null) =>
|
||||
set(() => ({ embeddedConversationId: conversationId })),
|
||||
}))
|
||||
|
||||
const getShareCodeFromRedirectUrl = (redirectUrl: string | null): string | null => {
|
||||
if (!redirectUrl || redirectUrl.length === 0)
|
||||
return null
|
||||
const url = new URL(`${window.location.origin}${decodeURIComponent(redirectUrl)}`)
|
||||
return url.pathname.split('/').pop() || null
|
||||
}
|
||||
const getShareCodeFromPathname = (pathname: string): string | null => {
|
||||
const code = pathname.split('/').pop() || null
|
||||
if (code === 'webapp-signin')
|
||||
return null
|
||||
return code
|
||||
}
|
||||
|
||||
const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
const isGlobalPending = useGlobalPublicStore(s => s.isGlobalPending)
|
||||
const updateWebAppAccessMode = useWebAppStore(state => state.updateWebAppAccessMode)
|
||||
const updateShareCode = useWebAppStore(state => state.updateShareCode)
|
||||
const updateEmbeddedUserId = useWebAppStore(state => state.updateEmbeddedUserId)
|
||||
const updateEmbeddedConversationId = useWebAppStore(state => state.updateEmbeddedConversationId)
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const redirectUrlParam = searchParams.get('redirect_url')
|
||||
const searchParamsString = searchParams.toString()
|
||||
|
||||
// Compute shareCode directly
|
||||
const shareCode = getShareCodeFromRedirectUrl(redirectUrlParam) || getShareCodeFromPathname(pathname)
|
||||
useEffect(() => {
|
||||
updateShareCode(shareCode)
|
||||
}, [shareCode, updateShareCode])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
const syncEmbeddedUserId = async () => {
|
||||
try {
|
||||
const { user_id, conversation_id } = await getProcessedSystemVariablesFromUrlParams()
|
||||
if (!cancelled) {
|
||||
updateEmbeddedUserId(user_id || null)
|
||||
updateEmbeddedConversationId(conversation_id || null)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
if (!cancelled) {
|
||||
updateEmbeddedUserId(null)
|
||||
updateEmbeddedConversationId(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
syncEmbeddedUserId()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [searchParamsString, updateEmbeddedUserId, updateEmbeddedConversationId])
|
||||
|
||||
const { isLoading, data: accessModeResult } = useGetWebAppAccessModeByCode(shareCode)
|
||||
|
||||
useEffect(() => {
|
||||
if (accessModeResult?.accessMode)
|
||||
updateWebAppAccessMode(accessModeResult.accessMode)
|
||||
}, [accessModeResult, updateWebAppAccessMode, shareCode])
|
||||
|
||||
if (isGlobalPending || isLoading) {
|
||||
return <div className='flex h-full w-full items-center justify-center'>
|
||||
<Loading />
|
||||
</div>
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default WebAppStoreProvider
|
||||
36
dify/web/context/workspace-context.tsx
Normal file
36
dify/web/context/workspace-context.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
import useSWR from 'swr'
|
||||
import { fetchWorkspaces } from '@/service/common'
|
||||
import type { IWorkspace } from '@/models/common'
|
||||
|
||||
export type WorkspacesContextValue = {
|
||||
workspaces: IWorkspace[]
|
||||
}
|
||||
|
||||
const WorkspacesContext = createContext<WorkspacesContextValue>({
|
||||
workspaces: [],
|
||||
})
|
||||
|
||||
type IWorkspaceProviderProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const WorkspaceProvider = ({
|
||||
children,
|
||||
}: IWorkspaceProviderProps) => {
|
||||
const { data } = useSWR({ url: '/workspaces' }, fetchWorkspaces)
|
||||
|
||||
return (
|
||||
<WorkspacesContext.Provider value={{
|
||||
workspaces: data?.workspaces || [],
|
||||
}}>
|
||||
{children}
|
||||
</WorkspacesContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useWorkspacesContext = () => useContext(WorkspacesContext)
|
||||
|
||||
export default WorkspacesContext
|
||||
Reference in New Issue
Block a user