+
{tSettings(($) => $.title)}
@@ -227,7 +85,6 @@ export function KnowledgeSettingsPage({ knowledgeSpaceId }: { knowledgeSpaceId:
{isPending &&
}
-
{!isPending && hasError && (
{tCommon(($) => $['api.actionFailed'])}
-
+
)}
-
- {!isPending && !hasError && spaceQuery.data && settingsQuery.data && (
-
setActiveDraftVersion(undefined)}
- onDraftStart={() =>
- setActiveDraftVersion(
- (current) => current ?? { conflict: basicDraftVersion, form: settingsFormVersion },
- )
- }
- />
- )}
+ {!isPending && !hasError && space && settings && }
)
}
+
+export function KnowledgeSettingsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }) {
+ return (
+
+
+
+ )
+}
diff --git a/web/features/new-rag/settings/retrieval-settings.tsx b/web/features/new-rag/settings/retrieval-settings.tsx
new file mode 100644
index 00000000000..ba46fd029af
--- /dev/null
+++ b/web/features/new-rag/settings/retrieval-settings.tsx
@@ -0,0 +1,657 @@
+'use client'
+
+import type { KnowledgeFsSettingsPayload } from '@dify/contracts/api/console/knowledge-fs/types.gen'
+import type { RetrievalSettingsDraft } from './model'
+import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
+import {
+ AlertDialog,
+ AlertDialogActions,
+ AlertDialogCancelButton,
+ AlertDialogConfirmButton,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogTitle,
+} from '@langgenius/dify-ui/alert-dialog'
+import {
+ NumberField,
+ NumberFieldControls,
+ NumberFieldDecrement,
+ NumberFieldGroup,
+ NumberFieldIncrement,
+ NumberFieldInput,
+} from '@langgenius/dify-ui/number-field'
+import { Slider } from '@langgenius/dify-ui/slider'
+import { Switch } from '@langgenius/dify-ui/switch'
+import { toast } from '@langgenius/dify-ui/toast'
+import { useMutation, useQuery } from '@tanstack/react-query'
+import { useAtomValue, useSetAtom } from 'jotai'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
+import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
+import { ModelSelector } from '@/app/components/header/account-setting/model-provider-page/model-selector'
+import { consoleQuery } from '@/service/client'
+import { KnowledgeModelReadinessNotice } from '../components/knowledge-model-readiness-notice'
+import { RetrievalModeSegmentedControl } from '../components/retrieval-mode-segmented-control'
+import {
+ modelFingerprint,
+ modelPayload,
+ retrievalDraftFromSettings,
+ retrievalFingerprint,
+ SCORE_THRESHOLD_MAX,
+ SCORE_THRESHOLD_MIN,
+ TOP_K_MAX,
+ TOP_K_MIN,
+} from './model'
+import {
+ invalidateKnowledgeSettingsAtom,
+ knowledgeSettingsSettingsAtom,
+ knowledgeSettingsSpaceAtom,
+} from './state/queries'
+import {
+ finishKnowledgeSettingsRetrievalDraftAtom,
+ setKnowledgeSettingsSavePendingAtom,
+ startKnowledgeSettingsRetrievalDraftAtom,
+} from './state/workflow'
+
+const REASONING_MODEL_LABEL_ID = 'knowledge-reasoning-model-label'
+const REASONING_MODEL_ERROR_ID = 'knowledge-reasoning-model-error'
+const EMBEDDING_MODEL_LABEL_ID = 'knowledge-embedding-model-label'
+const EMBEDDING_MODEL_ERROR_ID = 'knowledge-embedding-model-error'
+const RERANK_MODEL_LABEL_ID = 'knowledge-rerank-model-label'
+const RERANK_MODEL_ERROR_ID = 'knowledge-rerank-model-error'
+
+export function RetrievalSettingsSection() {
+ const { t } = useTranslation('dataset')
+ const { t: tCommon } = useTranslation('common')
+ const { t: tSettings } = useTranslation('datasetSettings')
+ const { t: tAppDebug } = useTranslation('appDebug')
+ const space = useAtomValue(knowledgeSettingsSpaceAtom)
+ const settings = useAtomValue(knowledgeSettingsSettingsAtom)
+ const invalidateSettings = useSetAtom(invalidateKnowledgeSettingsAtom)
+ const startDraftSession = useSetAtom(startKnowledgeSettingsRetrievalDraftAtom)
+ const finishDraftSession = useSetAtom(finishKnowledgeSettingsRetrievalDraftAtom)
+ const setSavePending = useSetAtom(setKnowledgeSettingsSavePendingAtom)
+ const { data: reasoningModelList } = useModelList(ModelTypeEnum.textGeneration)
+ const { data: embeddingModelList } = useModelList(ModelTypeEnum.textEmbedding)
+ const { data: rerankModelList } = useModelList(ModelTypeEnum.rerank)
+ const [draft, setDraft] = useState
()
+ const [pendingMigrationId, setPendingMigrationId] = useState()
+ const [pendingEmbeddingModel, setPendingEmbeddingModel] = useState()
+ const [embeddingDialogOpen, setEmbeddingDialogOpen] = useState(false)
+ const liveDraftRef = useRef(undefined)
+ const draftSessionActiveRef = useRef(false)
+ const embeddingBaselineRef = useRef(undefined)
+ const retrievalBaselineRef = useRef(undefined)
+ const settingsRevisionRef = useRef(undefined)
+ const queuedDraftRef = useRef(undefined)
+ const migratingDraftRef = useRef(undefined)
+ const activeMigrationIdRef = useRef(undefined)
+ const handledMigrationIdRef = useRef(undefined)
+ const saveInFlightRef = useRef(false)
+ const saveTimerRef = useRef | undefined>(undefined)
+ const settingsMutation = useMutation(
+ consoleQuery.knowledgeFs.spaces.byControlSpaceId.settings.patch.mutationOptions({
+ context: { silent: true },
+ }),
+ )
+ const migrationQuery = useQuery({
+ ...consoleQuery.knowledgeFs.spaces.byControlSpaceId.settings.migrations.byMigrationId.get.queryOptions(
+ {
+ input: {
+ params: {
+ control_space_id: space?.control_space_id ?? 'pending',
+ migration_id: pendingMigrationId ?? 'pending',
+ },
+ },
+ },
+ ),
+ enabled: Boolean(space && pendingMigrationId),
+ refetchInterval: (query) =>
+ query.state.data?.run_state === 'queued' || query.state.data?.run_state === 'running'
+ ? 2000
+ : false,
+ })
+
+ const showSaveSuccess = useCallback(
+ () => toast.success(tCommon(($) => $['api.actionSuccess'])),
+ [tCommon],
+ )
+ const showSaveError = useCallback(
+ (error?: unknown) =>
+ toast.error(
+ error instanceof Response && error.status === 403
+ ? t(($) => $['newKnowledge.permissionRestricted'])
+ : t(($) => $['newKnowledge.settings.saveFailed']),
+ ),
+ [t],
+ )
+
+ const beginDraft = (patch: Partial) => {
+ if (!settings) throw new Error('Knowledge settings are unavailable')
+ const serverDraft = retrievalDraftFromSettings(settings)
+ const current = liveDraftRef.current ?? draft ?? serverDraft
+ if (!draftSessionActiveRef.current) {
+ embeddingBaselineRef.current = modelFingerprint(current.embeddingModel)
+ retrievalBaselineRef.current = retrievalFingerprint(current)
+ if (!draft) settingsRevisionRef.current = settings.revision
+ draftSessionActiveRef.current = true
+ startDraftSession()
+ setSavePending({ owner: 'retrieval', pending: true })
+ }
+ const next = { ...current, ...patch }
+ liveDraftRef.current = next
+ setDraft(next)
+ return next
+ }
+
+ const saveDraft = async (nextDraft: RetrievalSettingsDraft) => {
+ if (!space || !settings) return 'skipped' as const
+ const nextEmbeddingFingerprint = modelFingerprint(nextDraft.embeddingModel)
+ const nextRetrievalFingerprint = retrievalFingerprint(nextDraft)
+ const embeddingBaseline =
+ embeddingBaselineRef.current ??
+ modelFingerprint(retrievalDraftFromSettings(settings).embeddingModel)
+ const retrievalBaseline =
+ retrievalBaselineRef.current ?? retrievalFingerprint(retrievalDraftFromSettings(settings))
+ const embeddingDirty = nextEmbeddingFingerprint !== embeddingBaseline
+ const retrievalDirty = nextRetrievalFingerprint !== retrievalBaseline
+ const initialModelSetup = !settings.active_profile_available
+ const invalid =
+ (embeddingDirty && !nextDraft.embeddingModel) ||
+ (retrievalDirty && (!nextDraft.reasoningModel || !nextDraft.rerankModel)) ||
+ (initialModelSetup && retrievalDirty && !nextDraft.embeddingModel) ||
+ (!initialModelSetup && embeddingDirty && retrievalDirty)
+ if (invalid || (!embeddingDirty && !retrievalDirty)) return 'skipped' as const
+
+ const body: KnowledgeFsSettingsPayload = {
+ expectedRevision: settingsRevisionRef.current ?? settings.revision,
+ }
+ if (embeddingDirty && nextDraft.embeddingModel)
+ body.embedding = modelPayload(nextDraft.embeddingModel)
+ if (retrievalDirty && nextDraft.reasoningModel && nextDraft.rerankModel) {
+ body.retrieval = {
+ defaultMode: nextDraft.retrievalMode,
+ reasoningModel: modelPayload(nextDraft.reasoningModel),
+ rerank: {
+ enabled: true,
+ model: modelPayload(nextDraft.rerankModel),
+ },
+ scoreThreshold: {
+ enabled: nextDraft.scoreThresholdEnabled,
+ stage: nextDraft.retrievalMode === 'research' ? 'mode-final' : 'rerank',
+ value: nextDraft.scoreThreshold,
+ },
+ topK: nextDraft.topK,
+ }
+ }
+
+ try {
+ const result = await settingsMutation.mutateAsync({
+ body,
+ params: { control_space_id: space.control_space_id },
+ })
+ settingsRevisionRef.current = result.settings.revision
+ if (result.migration) {
+ migratingDraftRef.current = nextDraft
+ activeMigrationIdRef.current = result.migration.id
+ handledMigrationIdRef.current = undefined
+ setPendingMigrationId(result.migration.id)
+ return 'migration' as const
+ }
+ if (embeddingDirty) embeddingBaselineRef.current = nextEmbeddingFingerprint
+ if (retrievalDirty) retrievalBaselineRef.current = nextRetrievalFingerprint
+ await invalidateSettings()
+ return 'saved' as const
+ } catch (error) {
+ showSaveError(error)
+ return 'failed' as const
+ }
+ }
+
+ const performSave = async (nextDraft: RetrievalSettingsDraft) => {
+ if (saveTimerRef.current) clearTimeout(saveTimerRef.current)
+ if (!space?.permission_keys.includes('knowledge_space_edit')) return
+ if (activeMigrationIdRef.current || saveInFlightRef.current) {
+ queuedDraftRef.current = nextDraft
+ return
+ }
+
+ saveInFlightRef.current = true
+ let candidate: RetrievalSettingsDraft | undefined = nextDraft
+ let result: Awaited> = 'skipped'
+ let didSave = false
+ try {
+ while (candidate) {
+ queuedDraftRef.current = undefined
+ result = await saveDraft(candidate)
+ if (result === 'saved') didSave = true
+ if (result === 'failed' || result === 'migration') break
+ candidate = queuedDraftRef.current
+ }
+ } finally {
+ saveInFlightRef.current = false
+ }
+
+ if (result === 'failed' || result === 'migration') {
+ if (result === 'failed') setSavePending({ owner: 'retrieval', pending: false })
+ return
+ }
+ const latestDraft = liveDraftRef.current
+ const latestSaved =
+ latestDraft &&
+ modelFingerprint(latestDraft.embeddingModel) === embeddingBaselineRef.current &&
+ retrievalFingerprint(latestDraft) === retrievalBaselineRef.current
+ if (!latestSaved) {
+ setSavePending({ owner: 'retrieval', pending: false })
+ return
+ }
+ if (didSave) showSaveSuccess()
+ liveDraftRef.current = latestDraft
+ if (latestDraft) setDraft(latestDraft)
+ draftSessionActiveRef.current = false
+ finishDraftSession()
+ setSavePending({ owner: 'retrieval', pending: false })
+ }
+
+ const scheduleSave = (nextDraft: RetrievalSettingsDraft) => {
+ if (saveTimerRef.current) clearTimeout(saveTimerRef.current)
+ saveTimerRef.current = setTimeout(() => void performSave(nextDraft), 400)
+ }
+
+ useEffect(() => {
+ if (!pendingMigrationId || handledMigrationIdRef.current === pendingMigrationId) return
+ const migration = migrationQuery.data
+ if (migration?.run_state === 'queued' || migration?.run_state === 'running') return
+
+ if (migration?.run_state === 'succeeded') {
+ handledMigrationIdRef.current = pendingMigrationId
+ const savedDraft = migratingDraftRef.current
+ if (savedDraft) {
+ embeddingBaselineRef.current = modelFingerprint(savedDraft.embeddingModel)
+ retrievalBaselineRef.current = retrievalFingerprint(savedDraft)
+ }
+ void invalidateSettings().then(() => {
+ migratingDraftRef.current = undefined
+ activeMigrationIdRef.current = undefined
+ setPendingMigrationId(undefined)
+ const queuedDraft = queuedDraftRef.current
+ queuedDraftRef.current = undefined
+ if (queuedDraft) {
+ void performSave(queuedDraft)
+ return
+ }
+ const latestDraft = liveDraftRef.current
+ if (latestDraft) setDraft(latestDraft)
+ draftSessionActiveRef.current = false
+ finishDraftSession()
+ setSavePending({ owner: 'retrieval', pending: false })
+ showSaveSuccess()
+ })
+ return
+ }
+
+ if (
+ migrationQuery.isError ||
+ migration?.run_state === 'failed' ||
+ migration?.run_state === 'canceled'
+ ) {
+ handledMigrationIdRef.current = pendingMigrationId
+ migratingDraftRef.current = undefined
+ activeMigrationIdRef.current = undefined
+ // oxlint-disable-next-line eslint-react/set-state-in-effect -- A terminal remote migration retires the local polling session.
+ setPendingMigrationId(undefined)
+ setSavePending({ owner: 'retrieval', pending: false })
+ showSaveError()
+ }
+ // oxlint-disable-next-line react-hooks/exhaustive-deps -- Migration completion is keyed by the observer state; queued saves use the latest ref snapshot.
+ }, [
+ finishDraftSession,
+ invalidateSettings,
+ migrationQuery.data,
+ migrationQuery.isError,
+ pendingMigrationId,
+ setSavePending,
+ showSaveError,
+ showSaveSuccess,
+ ])
+
+ useEffect(
+ () => () => {
+ if (saveTimerRef.current) clearTimeout(saveTimerRef.current)
+ },
+ [],
+ )
+
+ if (!space || !settings) return null
+ const current = draft ?? retrievalDraftFromSettings(settings)
+ const canEdit = space.permission_keys.includes('knowledge_space_edit')
+ const initialModelSetup = !settings.active_profile_available
+ const embeddingDirty = draft
+ ? modelFingerprint(current.embeddingModel) !== embeddingBaselineRef.current
+ : false
+ const retrievalDirty = draft
+ ? retrievalFingerprint(current) !== retrievalBaselineRef.current
+ : false
+ const retrievalFieldsDisabled = !canEdit || (!initialModelSetup && embeddingDirty)
+ const readinessFieldLabel = (field: (typeof settings.issues)[number]['field']) => {
+ if (field === 'embedding') return tSettings(($) => $['form.embeddingModel'])
+ if (field === 'reasoning') return tCommon(($) => $['modelProvider.systemReasoningModel.key'])
+ if (field === 'rerank') return tCommon(($) => $['modelProvider.rerankModel.key'])
+ return t(($) => $['newKnowledge.overview.attention.modelReadiness.bindingMissing'])
+ }
+
+ return (
+ <>
+ {settings.configuration_state !== 'pending-validation' &&
+ (settings.configuration_state !== 'active' || settings.issues.length > 0) && (
+ 0
+ ? settings.issues.map(({ field }) => readinessFieldLabel(field)).join(' ยท ')
+ : settings.active_profile_available
+ ? t(($) => $['newKnowledge.overview.attention.modelReadiness.description'])
+ : undefined
+ }
+ title={
+ settings.configuration_state === 'validation-failed'
+ ? tCommon(($) => $['api.actionFailed'])
+ : tCommon(($) => $['modelProvider.toBeConfigured'])
+ }
+ tone={settings.configuration_state === 'validation-failed' ? 'destructive' : 'warning'}
+ />
+ )}
+
+
+
+
+
+ {t(($) => $['newKnowledge.settings.retrievalTitle'])}
+
+
+ {t(($) => $['newKnowledge.settings.retrievalDescription'])}
+
+
+
+
+
+ {t(($) => $['newKnowledge.settings.systemReasoningModelLabel'])}
+
+ *
+
+
+
{
+ const next = beginDraft({ reasoningModel })
+ void performSave(next)
+ }}
+ />
+ {!current.reasoningModel && (
+
+ {t(($) => $['newKnowledge.settings.systemReasoningModelRequired'])}
+
+ )}
+
+
+
+
+ {t(($) => $['newKnowledge.settings.embeddingModelLabel'])}
+
+ *
+
+
+
{
+ if ((space.technical_summary?.document_count ?? 0) > 0) {
+ setPendingEmbeddingModel(model)
+ setEmbeddingDialogOpen(true)
+ return
+ }
+ const next = beginDraft({ embeddingModel: model })
+ void performSave(next)
+ }}
+ />
+ {!current.embeddingModel && (
+
+ {t(($) => $['newKnowledge.settings.embeddingModelRequired'])}
+
+ )}
+ {embeddingDirty && (space.technical_summary?.document_count ?? 0) > 0 && (
+
+
+ {t(($) => $['newKnowledge.settings.embeddingChangeWarning'])}
+
+ )}
+
+
+
+
+ {tCommon(($) => $['modelProvider.rerankModel.key'])}
+
+ *
+
+
+
{
+ const next = beginDraft({ rerankModel })
+ void performSave(next)
+ }}
+ />
+ {!current.rerankModel && (
+
+ {t(($) => $['newKnowledge.settings.rerankModelRequired'])}
+
+ )}
+
+
+
+
+ {t(($) => $['newKnowledge.settings.retrievalDepth'])}
+
+
{
+ const next = beginDraft({ retrievalMode })
+ void performSave(next)
+ }}
+ />
+
+
+
+
+
+
+ {
+ const next = beginDraft({ topK: value ?? TOP_K_MIN })
+ scheduleSave(next)
+ }}
+ onValueCommitted={() => {
+ if (liveDraftRef.current) void performSave(liveDraftRef.current)
+ }}
+ >
+
+ $['newKnowledge.settings.topKLabel'])}
+ autoComplete="off"
+ />
+
+
+
+
+
+
+ $['newKnowledge.settings.topKLabel'])}
+ min={TOP_K_MIN}
+ max={TOP_K_MAX}
+ value={current.topK}
+ disabled={retrievalFieldsDisabled}
+ onValueChange={(topK) => scheduleSave(beginDraft({ topK }))}
+ />
+
+
+ {t(($) => $['newKnowledge.settings.topKMinimum'])}
+
+
+
+
+
+ $['datasetConfig.score_threshold'])}
+ checked={current.scoreThresholdEnabled}
+ disabled={retrievalFieldsDisabled || !current.rerankModel}
+ onCheckedChange={(scoreThresholdEnabled) => {
+ const next = beginDraft({ scoreThresholdEnabled })
+ void performSave(next)
+ }}
+ />
+
+
+
+
+ scheduleSave(beginDraft({ scoreThreshold: value ?? SCORE_THRESHOLD_MIN }))
+ }
+ onValueCommitted={() => {
+ if (liveDraftRef.current) void performSave(liveDraftRef.current)
+ }}
+ >
+
+ $['datasetConfig.score_threshold'])}
+ autoComplete="off"
+ />
+
+
+
+
+
+
+ $['datasetConfig.score_threshold'])}
+ min={SCORE_THRESHOLD_MIN}
+ max={SCORE_THRESHOLD_MAX}
+ step={0.01}
+ value={current.scoreThreshold}
+ disabled={retrievalFieldsDisabled || !current.scoreThresholdEnabled}
+ onValueChange={(scoreThreshold) => scheduleSave(beginDraft({ scoreThreshold }))}
+ />
+
+
+ {t(($) => $['newKnowledge.settings.scoreRange'])}
+
+
+
+
+
+
+ {
+ setEmbeddingDialogOpen(open)
+ if (!open) setPendingEmbeddingModel(undefined)
+ }}
+ >
+
+
+
+ {tSettings(($) => $['form.embeddingModel'])}
+
+
+ {t(($) => $['newKnowledge.settings.embeddingChangeWarning'])}
+
+
+
+ setPendingEmbeddingModel(undefined)}>
+ {tCommon(($) => $['operation.cancel'])}
+
+ {
+ const model = pendingEmbeddingModel
+ setEmbeddingDialogOpen(false)
+ setPendingEmbeddingModel(undefined)
+ if (!model) return
+ const next = beginDraft({ embeddingModel: model })
+ void performSave(next)
+ }}
+ >
+ {tCommon(($) => $['operation.confirm'])}
+
+
+
+
+ >
+ )
+}
diff --git a/web/features/new-rag/settings/settings-field-row.tsx b/web/features/new-rag/settings/settings-field-row.tsx
new file mode 100644
index 00000000000..1627564b481
--- /dev/null
+++ b/web/features/new-rag/settings/settings-field-row.tsx
@@ -0,0 +1,12 @@
+import type { ReactNode } from 'react'
+
+export function SettingsFieldRow({ children, label }: { children: ReactNode; label: ReactNode }) {
+ return (
+
+
+ {label}
+
+
{children}
+
+ )
+}
diff --git a/web/features/new-rag/settings/state/boundary.tsx b/web/features/new-rag/settings/state/boundary.tsx
new file mode 100644
index 00000000000..f402f16c5a0
--- /dev/null
+++ b/web/features/new-rag/settings/state/boundary.tsx
@@ -0,0 +1,29 @@
+'use client'
+
+import type { ReactNode } from 'react'
+import { ScopeProvider } from 'jotai-scope'
+import { useHydrateAtoms } from 'jotai/utils'
+import { knowledgeSettingsSpaceIdAtom } from './inputs'
+import { knowledgeSettingsScopedAtoms } from './workflow'
+
+export function KnowledgeSettingsStateBoundary({
+ children,
+ knowledgeSpaceId,
+}: {
+ children: ReactNode
+ knowledgeSpaceId: string
+}) {
+ useHydrateAtoms([[knowledgeSettingsSpaceIdAtom, knowledgeSpaceId]], {
+ dangerouslyForceHydrate: true,
+ })
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/web/features/new-rag/settings/state/inputs.ts b/web/features/new-rag/settings/state/inputs.ts
new file mode 100644
index 00000000000..f8ac8d0e614
--- /dev/null
+++ b/web/features/new-rag/settings/state/inputs.ts
@@ -0,0 +1,5 @@
+import { atomWithLazy } from 'jotai/utils'
+
+export const knowledgeSettingsSpaceIdAtom = atomWithLazy(() => {
+ throw new Error('Missing knowledge settings space id')
+})
diff --git a/web/features/new-rag/settings/state/queries.ts b/web/features/new-rag/settings/state/queries.ts
new file mode 100644
index 00000000000..b40f66c0d22
--- /dev/null
+++ b/web/features/new-rag/settings/state/queries.ts
@@ -0,0 +1,88 @@
+import { atom } from 'jotai'
+import { atomWithQuery } from 'jotai-tanstack-query'
+import { selectAtom } from 'jotai/utils'
+import { consoleQuery } from '@/service/client'
+import { knowledgeSettingsSpaceIdAtom } from './inputs'
+
+const spaceQueryAtom = atomWithQuery((get) =>
+ consoleQuery.knowledgeFs.spaces.byControlSpaceId.get.queryOptions({
+ input: { params: { control_space_id: get(knowledgeSettingsSpaceIdAtom) } },
+ }),
+)
+
+const settingsQueryAtom = atomWithQuery((get) =>
+ consoleQuery.knowledgeFs.spaces.byControlSpaceId.settings.get.queryOptions({
+ input: { params: { control_space_id: get(knowledgeSettingsSpaceIdAtom) } },
+ }),
+)
+
+export const knowledgeSettingsSpaceAtom = selectAtom(spaceQueryAtom, (query) => query.data)
+export const knowledgeSettingsSettingsAtom = selectAtom(settingsQueryAtom, (query) => query.data)
+
+export const knowledgeSettingsCanManageAccessAtom = atom(
+ (get) =>
+ get(knowledgeSettingsSpaceAtom)?.permission_keys.includes('knowledge_space_access_config') ??
+ false,
+)
+
+const permissionsQueryAtom = atomWithQuery((get) => ({
+ ...consoleQuery.knowledgeFs.spaces.byControlSpaceId.permissions.get.queryOptions({
+ input: { params: { control_space_id: get(knowledgeSettingsSpaceIdAtom) } },
+ }),
+ enabled: get(knowledgeSettingsCanManageAccessAtom),
+}))
+
+const externalAccessQueryAtom = atomWithQuery((get) => ({
+ ...consoleQuery.knowledgeFs.spaces.byControlSpaceId.externalAccess.get.queryOptions({
+ input: { params: { control_space_id: get(knowledgeSettingsSpaceIdAtom) } },
+ }),
+ enabled: get(knowledgeSettingsCanManageAccessAtom),
+}))
+
+export const knowledgeSettingsPermissionsAtom = selectAtom(
+ permissionsQueryAtom,
+ (query) => query.data?.data ?? [],
+)
+export const knowledgeSettingsExternalAccessAtom = selectAtom(
+ externalAccessQueryAtom,
+ (query) => query.data,
+)
+
+export const knowledgeSettingsIsPendingAtom = atom((get) => {
+ const canManageAccess = get(knowledgeSettingsCanManageAccessAtom)
+ return (
+ get(spaceQueryAtom).isPending ||
+ get(settingsQueryAtom).isPending ||
+ (canManageAccess &&
+ (get(permissionsQueryAtom).isPending || get(externalAccessQueryAtom).isPending))
+ )
+})
+
+export const knowledgeSettingsHasErrorAtom = atom((get) => {
+ const canManageAccess = get(knowledgeSettingsCanManageAccessAtom)
+ return (
+ get(spaceQueryAtom).isError ||
+ get(settingsQueryAtom).isError ||
+ (canManageAccess && (get(permissionsQueryAtom).isError || get(externalAccessQueryAtom).isError))
+ )
+})
+
+export const retryKnowledgeSettingsAtom = atom(null, async (get) => {
+ const requests: Promise[] = [
+ get(spaceQueryAtom).refetch(),
+ get(settingsQueryAtom).refetch(),
+ ]
+ if (get(knowledgeSettingsCanManageAccessAtom))
+ requests.push(get(permissionsQueryAtom).refetch(), get(externalAccessQueryAtom).refetch())
+ await Promise.all(requests)
+})
+
+export const invalidateKnowledgeSettingsAtom = atom(null, async (get) => {
+ const requests: Promise[] = [
+ get(spaceQueryAtom).refetch(),
+ get(settingsQueryAtom).refetch(),
+ ]
+ if (get(knowledgeSettingsCanManageAccessAtom))
+ requests.push(get(permissionsQueryAtom).refetch(), get(externalAccessQueryAtom).refetch())
+ await Promise.all(requests)
+})
diff --git a/web/features/new-rag/settings/state/workflow.ts b/web/features/new-rag/settings/state/workflow.ts
new file mode 100644
index 00000000000..6af3e386c4b
--- /dev/null
+++ b/web/features/new-rag/settings/state/workflow.ts
@@ -0,0 +1,47 @@
+import { atom } from 'jotai'
+
+const basicDraftActiveAtom = atom(false)
+const retrievalDraftActiveAtom = atom(false)
+const pendingSaveOwnersAtom = atom>(new globalThis.Set())
+
+export const knowledgeSettingsScopedAtoms = [
+ basicDraftActiveAtom,
+ retrievalDraftActiveAtom,
+ pendingSaveOwnersAtom,
+] as const
+
+export const knowledgeSettingsHasUnsavedWorkAtom = atom(
+ (get) => get(basicDraftActiveAtom) || get(retrievalDraftActiveAtom),
+)
+
+export const knowledgeSettingsHasPendingSaveAtom = atom(
+ (get) => get(pendingSaveOwnersAtom).size > 0,
+)
+
+export const setKnowledgeSettingsSavePendingAtom = atom(
+ null,
+ (_get, set, { owner, pending }: { owner: string; pending: boolean }) => {
+ set(pendingSaveOwnersAtom, (current) => {
+ const owners = new globalThis.Set(current)
+ if (pending) owners.add(owner)
+ else owners.delete(owner)
+ return owners
+ })
+ },
+)
+
+export const startKnowledgeSettingsBasicDraftAtom = atom(null, (_get, set) => {
+ set(basicDraftActiveAtom, true)
+})
+
+export const finishKnowledgeSettingsBasicDraftAtom = atom(null, (_get, set) => {
+ set(basicDraftActiveAtom, false)
+})
+
+export const startKnowledgeSettingsRetrievalDraftAtom = atom(null, (_get, set) => {
+ set(retrievalDraftActiveAtom, true)
+})
+
+export const finishKnowledgeSettingsRetrievalDraftAtom = atom(null, (_get, set) => {
+ set(retrievalDraftActiveAtom, false)
+})