dify/web/features/new-rag/new-knowledge-list.tsx
Stephen Zhou 2a0661a769
feat(dataset): add New Knowledge creation flow (#39315)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-23 07:25:14 +00:00

210 lines
8.8 KiB
TypeScript

'use client'
import { Button } from '@langgenius/dify-ui/button'
import { toast } from '@langgenius/dify-ui/toast'
import { useInfiniteQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SearchInput } from '@/app/components/base/search-input'
import ExternalAPIPanel from '@/app/components/datasets/external-api/external-api-panel'
import ServiceApi from '@/app/components/datasets/extra-info/service-api'
import { useExternalApiPanel } from '@/context/external-api-panel-context'
import { workspacePermissionKeysAtom } from '@/context/permission-state'
import Link from '@/next/link'
import { consoleQuery } from '@/service/client'
import { useDatasetApiBaseUrl } from '@/service/knowledge/use-dataset'
import { hasPermission } from '@/utils/permission'
import { KnowledgeSpaceCard } from './components/knowledge-space-card'
import { KnowledgeViewSwitcher } from './components/knowledge-view-switcher'
import {
KNOWLEDGE_SPACE_GRID_CLASS_NAME,
NewKnowledgeEmptyState,
NewKnowledgeLoadingState,
NewKnowledgePageState,
} from './components/new-knowledge-list-states'
const PAGE_SIZE = 30
function isUnavailableError(error: unknown) {
if (!error || typeof error !== 'object') return false
const status = 'status' in error ? error.status : undefined
if (status === 404 || status === 503) return true
const data = 'data' in error ? error.data : undefined
if (!data || typeof data !== 'object') return false
const dataStatus = 'status' in data ? data.status : undefined
return dataStatus === 404 || dataStatus === 503
}
function MetadataFilter({ label, onClick }: { label: string; onClick: () => void }) {
return (
<button
type="button"
className="flex h-8 items-center rounded-lg border-[0.5px] border-transparent bg-components-input-bg-normal px-2 text-text-tertiary outline-hidden hover:bg-state-base-hover-alt focus-visible:bg-state-base-hover-alt focus-visible:ring-2 focus-visible:ring-state-accent-solid"
onClick={onClick}
>
<span className="px-1 system-sm-regular">{label}</span>
<span aria-hidden className="i-ri-arrow-down-s-line size-4" />
</button>
)
}
export function NewKnowledgeList({
view,
onViewChange,
}: {
view: 'legacy' | 'new'
onViewChange: (value: 'legacy' | 'new') => void
}) {
const { t } = useTranslation('dataset')
const { t: tCommon } = useTranslation('common')
const { data: apiBaseInfo } = useDatasetApiBaseUrl()
const { showExternalApiPanel, setShowExternalApiPanel } = useExternalApiPanel()
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canCreate = hasPermission(workspacePermissionKeys, 'dataset.create_and_management')
const canConnect = hasPermission(workspacePermissionKeys, 'dataset.external.connect')
const filtersUnavailable = t(($) => $['newKnowledge.filtersUnavailable'])
const showFilterBoundary = () => toast.info(filtersUnavailable)
const createLabel = tCommon(($) => $['operation.create'])
const [searchValue, setSearchValue] = useState('')
const knowledgeSpacesQuery = useInfiniteQuery(
consoleQuery.knowledgeFs.listKnowledgeSpaces.infiniteOptions({
input: (pageParam) => ({
query: {
limit: PAGE_SIZE,
...(typeof pageParam === 'string' ? { cursor: pageParam } : {}),
},
}),
getNextPageParam: (lastPage) => lastPage.nextCursor,
initialPageParam: null as string | null,
}),
)
const knowledgeSpaces = knowledgeSpacesQuery.data?.pages.flatMap((page) => page.items) ?? []
const normalizedSearchValue = searchValue.trim().toLocaleLowerCase()
const visibleKnowledgeSpaces = normalizedSearchValue
? knowledgeSpaces.filter(
(knowledgeSpace) =>
knowledgeSpace.name.toLocaleLowerCase().includes(normalizedSearchValue) ||
knowledgeSpace.description?.toLocaleLowerCase().includes(normalizedSearchValue),
)
: knowledgeSpaces
return (
<section
aria-label={t(($) => $['newKnowledge.new'])}
className="relative flex grow flex-col overflow-y-auto bg-background-body"
>
<header className="sticky top-0 z-10 flex flex-col gap-[14px] bg-background-body px-4 pt-4 pb-2 sm:px-8">
<div className="flex min-h-6 flex-wrap items-center justify-between gap-2">
<div className="flex min-w-0 flex-wrap items-center gap-2">
<h1 className="text-[18px]/[21.6px] font-semibold text-text-primary">
{t(($) => $.knowledge)}
</h1>
<KnowledgeViewSwitcher value={view} onChange={onViewChange} />
</div>
<div className="flex max-w-full shrink-0 flex-wrap items-center gap-2">
{canConnect && (
<Button
variant="ghost"
size="small"
className="gap-1 overflow-hidden px-1.5 text-text-tertiary"
onClick={() => setShowExternalApiPanel(true)}
>
<span
aria-hidden
className="i-custom-vender-solid-development-api-connection-mod size-3.5 shrink-0"
/>
<span className="px-0.5 system-xs-medium">{t(($) => $.externalAPIPanelTitle)}</span>
</Button>
)}
<ServiceApi apiBaseUrl={apiBaseInfo?.api_base_url ?? ''} />
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
<MetadataFilter label={t(($) => $['newKnowledge.tags'])} onClick={showFilterBoundary} />
<MetadataFilter
label={t(($) => $['newKnowledge.creators'])}
onClick={showFilterBoundary}
/>
<SearchInput
className="w-full min-w-0 sm:w-[200px]"
value={searchValue}
onValueChange={setSearchValue}
/>
</div>
{canCreate && (
<div className="flex items-center gap-1">
<Button
render={<Link href="/datasets/new/create" />}
variant="primary"
size="medium"
className="gap-0.5 px-2 shadow-xs"
>
<span aria-hidden className="i-ri-add-line size-4 shrink-0" />
<span className="pl-1">{createLabel}</span>
</Button>
</div>
)}
</div>
</header>
<div className="px-4 pt-2 pb-8 sm:px-8">
{knowledgeSpacesQuery.isPending ? (
<NewKnowledgeLoadingState />
) : knowledgeSpacesQuery.error && !knowledgeSpacesQuery.data ? (
isUnavailableError(knowledgeSpacesQuery.error) ? (
<NewKnowledgePageState
title={t(($) => $['newKnowledge.unavailableTitle'])}
description={t(($) => $['newKnowledge.unavailableDescription'])}
/>
) : (
<NewKnowledgePageState
title={t(($) => $['newKnowledge.errorTitle'])}
description={t(($) => $['newKnowledge.errorDescription'])}
action={
<Button onClick={() => void knowledgeSpacesQuery.refetch()}>
{tCommon(($) => $['operation.retry'])}
</Button>
}
/>
)
) : knowledgeSpaces.length === 0 ? (
<NewKnowledgeEmptyState canConnect={canConnect} canCreate={canCreate} />
) : (
<>
<ul className={KNOWLEDGE_SPACE_GRID_CLASS_NAME} aria-label={t(($) => $.knowledge)}>
{visibleKnowledgeSpaces.map((knowledgeSpace) => (
<KnowledgeSpaceCard key={knowledgeSpace.id} knowledgeSpace={knowledgeSpace} />
))}
</ul>
{knowledgeSpacesQuery.isFetchNextPageError ? (
<div className="mt-6 flex items-center justify-center gap-3" role="alert">
<span>{t(($) => $['newKnowledge.errorDescription'])}</span>
<Button onClick={() => void knowledgeSpacesQuery.fetchNextPage()}>
{tCommon(($) => $['operation.retry'])}
</Button>
</div>
) : knowledgeSpacesQuery.hasNextPage ? (
<div className="mt-6 flex justify-center">
<Button
loading={knowledgeSpacesQuery.isFetchingNextPage}
onClick={() => void knowledgeSpacesQuery.fetchNextPage()}
>
{t(($) => $['newKnowledge.loadMore'])}
</Button>
</div>
) : null}
</>
)}
</div>
{showExternalApiPanel && canConnect && (
<ExternalAPIPanel
canManageExternalKnowledgeApi={canConnect}
onClose={() => setShowExternalApiPanel(false)}
/>
)}
</section>
)
}