'use client' import type { KnowledgeSpaceCreationResponse } from '@dify/contracts/knowledge-fs/types.gen' import type { CreateKnowledgeExitReason } from './components/create-knowledge-exit-dialog' import type { KnowledgeVisibility } from './create-knowledge-workflow' import type { NewKnowledgeSourceDraft, NewKnowledgeStartMode } from './routes' import { Button } from '@langgenius/dify-ui/button' import { Dialog, DialogBackdrop, DialogPopup, DialogPortal, DialogTitle, } from '@langgenius/dify-ui/dialog' import { Field, FieldControl, FieldDescription, FieldError, FieldLabel, } from '@langgenius/dify-ui/field' import { Form } from '@langgenius/dify-ui/form' import { RadioGroup } from '@langgenius/dify-ui/radio' import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectLabel, SelectTrigger, } from '@langgenius/dify-ui/select' import { Textarea } from '@langgenius/dify-ui/textarea' import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import { useCallback, useEffect, useId, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { useRouter, useSearchParams } from '@/next/navigation' import { consoleQuery } from '@/service/client' import { DatasetACLPermission, hasPermission } from '@/utils/permission' import { KnowledgeIllustration, StartMode } from './components/create-knowledge-dialog-parts' import { CreateKnowledgeExitDialog } from './components/create-knowledge-exit-dialog' import { createKnowledge, DESCRIPTION_MAX_LENGTH, isDefinitiveCreationRejection, KnowledgeCreationError, NAME_MAX_LENGTH, } from './create-knowledge-workflow' import { CreateSourceSetup } from './create-source-setup' import { createRequestId } from './request-id' import { createNewKnowledgeSourceDraft, isValidWebsiteSourceDraft, newKnowledgeAddSourcePath, newKnowledgeDetailPath, newKnowledgeListPath, newKnowledgeSourceDraftStorageKey, } from './routes' function normalizeStartMode(value: string | null): NewKnowledgeStartMode { return value === 'source' ? value : 'empty' } export function CreateKnowledgePage() { const { t } = useTranslation('dataset') const { t: tCommon } = useTranslation('common') const router = useRouter() const searchParams = useSearchParams() const queryClient = useQueryClient() const dialogTitleId = useId() const permissionDescriptionId = useId() const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) const canConfigureAccess = hasPermission( workspacePermissionKeys, DatasetACLPermission.AccessConfig, ) const defaultVisibility: KnowledgeVisibility = canConfigureAccess ? 'all_members' : 'only_me' const [name, setName] = useState('') const [description, setDescription] = useState('') const [visibility, setVisibility] = useState(defaultVisibility) const initialStartMode = normalizeStartMode(searchParams.get('start')) const [startMode, setStartMode] = useState(initialStartMode) const [sourceDraft, setSourceDraft] = useState(() => createNewKnowledgeSourceDraft('websiteCrawl'), ) const sourceDraftsRef = useRef< Partial> >({}) const [createdKnowledge, setCreatedKnowledge] = useState() const [submissionLocked, setSubmissionLocked] = useState(false) const [exitReason, setExitReason] = useState(null) const idempotencyKeyRef = useRef(undefined) const historyGuardArmedRef = useRef(false) const browserBackExitRef = useRef(false) const pendingNavigationRef = useRef(undefined) const createMutation = useMutation({ mutationFn: createKnowledge }) const sourceSubmissionBlocked = startMode === 'source' && (sourceDraft.sourceType === 'websiteCrawl' ? !isValidWebsiteSourceDraft(sourceDraft) : !sourceDraft.sourceName.trim()) const sourceDraftChanged = Object.values({ ...sourceDraftsRef.current, [sourceDraft.sourceType]: sourceDraft, }).some( (draft) => JSON.stringify(draft) !== JSON.stringify(createNewKnowledgeSourceDraft(draft.sourceType)), ) const hasUnsavedChanges = Boolean( name || description || visibility !== defaultVisibility || startMode !== initialStartMode || sourceDraftChanged || createdKnowledge, ) const armHistoryGuard = useCallback(() => { globalThis.history.pushState(globalThis.history.state, '', globalThis.location.href) historyGuardArmedRef.current = true }, []) const replaceAfterHistoryGuard = useCallback( (path: string) => { if (!historyGuardArmedRef.current) { router.replace(path) return } pendingNavigationRef.current = path globalThis.history.back() }, [router], ) useEffect(() => { if ( !hasUnsavedChanges || historyGuardArmedRef.current || browserBackExitRef.current || pendingNavigationRef.current ) return armHistoryGuard() }, [armHistoryGuard, hasUnsavedChanges]) useEffect(() => { const handlePopState = () => { if (!historyGuardArmedRef.current) return historyGuardArmedRef.current = false const pendingNavigation = pendingNavigationRef.current if (pendingNavigation) { pendingNavigationRef.current = undefined router.replace(pendingNavigation) return } if (!hasUnsavedChanges) { router.replace(newKnowledgeListPath) return } browserBackExitRef.current = true setExitReason(createdKnowledge ? 'partial' : 'discard') } globalThis.addEventListener('popstate', handlePopState) return () => globalThis.removeEventListener('popstate', handlePopState) }, [createdKnowledge, hasUnsavedChanges, router]) useEffect(() => { if (!hasUnsavedChanges) return const handleBeforeUnload = (event: BeforeUnloadEvent) => { event.preventDefault() event.returnValue = '' } globalThis.addEventListener('beforeunload', handleBeforeUnload) return () => globalThis.removeEventListener('beforeunload', handleBeforeUnload) }, [hasUnsavedChanges]) const resetUnsubmittedError = () => { if (!submissionLocked) createMutation.reset() } const requestClose = () => { if (createMutation.isPending) return if (createdKnowledge) { setExitReason('partial') return } if (hasUnsavedChanges) { setExitReason('discard') return } replaceAfterHistoryGuard(newKnowledgeListPath) } const confirmExit = () => { const confirmedReason = exitReason setExitReason(null) if (confirmedReason === 'partial' && createdKnowledge) { browserBackExitRef.current = false replaceAfterHistoryGuard(newKnowledgeDetailPath(createdKnowledge.id)) return } browserBackExitRef.current = false replaceAfterHistoryGuard(newKnowledgeListPath) } const cancelExit = () => { setExitReason(null) if (!browserBackExitRef.current) return browserBackExitRef.current = false armHistoryGuard() } const handleSubmit = async () => { if (createMutation.isPending || sourceSubmissionBlocked) return const normalizedName = name.trim() const normalizedDescription = description.trim() if (!normalizedName) return idempotencyKeyRef.current ??= createRequestId() setSubmissionLocked(true) try { const created = await createMutation.mutateAsync({ existingKnowledge: createdKnowledge, description: normalizedDescription, idempotencyKey: idempotencyKeyRef.current, name: normalizedName, onCreated: (knowledgeSpace) => { setCreatedKnowledge(knowledgeSpace) void queryClient.invalidateQueries({ queryKey: consoleQuery.knowledgeFs.listKnowledgeSpaces.key(), }) }, visibility, }) if (startMode === 'source') { try { const sourceDraftKey = createRequestId() globalThis.sessionStorage.setItem( newKnowledgeSourceDraftStorageKey(sourceDraftKey), JSON.stringify(sourceDraft), ) replaceAfterHistoryGuard( newKnowledgeAddSourcePath(created.id, sourceDraft.sourceType, sourceDraftKey), ) } catch { toast.error(t(($) => $['newKnowledge.addSourceFailed'])) } return } replaceAfterHistoryGuard(newKnowledgeDetailPath(created.id)) } catch (error) { if (error instanceof KnowledgeCreationError && error.createdKnowledge) setCreatedKnowledge(error.createdKnowledge) if ( error instanceof KnowledgeCreationError && error.stage === 'create' && isDefinitiveCreationRejection(error.originalError) ) { idempotencyKeyRef.current = undefined setSubmissionLocked(false) } // The mutation state renders a retryable, localized error without exposing upstream details. } } return ( { if (!open) requestClose() }} >
{t(($) => $['newKnowledge.createTitle'])}
{ if (typeof value === 'string' && value.length > 0 && !value.trim()) return t(($) => $['newKnowledge.nameRequired']) return null }} > {t(($) => $['newKnowledge.name'])} * $['newKnowledge.namePlaceholder'])} required value={name} onValueChange={(value) => { setName(value) resetUnsubmittedError() }} /> {t(($) => $['newKnowledge.nameRequired'])} {t(($) => $['newKnowledge.description'])}