chore(web): new lint setup (#30020)

Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
Stephen Zhou
2025-12-23 16:58:55 +08:00
committed by GitHub
parent 9701a2994b
commit f2842da397
3356 changed files with 85046 additions and 81278 deletions

View File

@ -14,7 +14,7 @@ vi.mock('next/navigation', () => ({
// Mock next/link to capture href
vi.mock('next/link', () => ({
default: ({ children, href, replace }: { children: React.ReactNode; href: string; replace?: boolean }) => (
default: ({ children, href, replace }: { children: React.ReactNode, href: string, replace?: boolean }) => (
<a href={href} data-replace={replace}>
{children}
</a>

View File

@ -1,9 +1,9 @@
import React, { useMemo } from 'react'
import Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next'
import { useParams } from 'next/navigation'
import { RiArrowRightLine } from '@remixicon/react'
import Link from 'next/link'
import { useParams } from 'next/navigation'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Checkbox from '@/app/components/base/checkbox'
type ActionsProps = {
@ -29,58 +29,62 @@ const Actions = ({
const { datasetId } = useParams()
const indeterminate = useMemo(() => {
if (!showSelect) return false
if (selectedOptions === undefined || totalOptions === undefined) return false
if (!showSelect)
return false
if (selectedOptions === undefined || totalOptions === undefined)
return false
return selectedOptions > 0 && selectedOptions < totalOptions
}, [showSelect, selectedOptions, totalOptions])
const checked = useMemo(() => {
if (!showSelect) return false
if (selectedOptions === undefined || totalOptions === undefined) return false
if (!showSelect)
return false
if (selectedOptions === undefined || totalOptions === undefined)
return false
return selectedOptions > 0 && selectedOptions === totalOptions
}, [showSelect, selectedOptions, totalOptions])
return (
<div className='flex items-center gap-x-2 overflow-hidden'>
<div className="flex items-center gap-x-2 overflow-hidden">
{showSelect && (
<>
<div className='flex shrink-0 items-center gap-x-2 py-[3px] pl-4 pr-2'>
<div className="flex shrink-0 items-center gap-x-2 py-[3px] pl-4 pr-2">
<Checkbox
onCheck={onSelectAll}
indeterminate={indeterminate}
checked={checked}
/>
<span className='system-sm-medium text-text-accent'>
<span className="system-sm-medium text-text-accent">
{t('common.operation.selectAll')}
</span>
</div>
{tip && (
<div title={tip} className='system-xs-regular max-w-full truncate text-text-tertiary'>
<div title={tip} className="system-xs-regular max-w-full truncate text-text-tertiary">
{tip}
</div>
)}
</>
)}
<div className='flex grow items-center justify-end gap-x-2'>
<div className="flex grow items-center justify-end gap-x-2">
<Link
href={`/datasets/${datasetId}/documents`}
replace
>
<Button
variant='ghost'
className='px-3 py-2'
variant="ghost"
className="px-3 py-2"
>
{t('common.operation.cancel')}
</Button>
</Link>
<Button
disabled={disabled}
variant='primary'
variant="primary"
onClick={handleNextStep}
className='gap-x-0.5'
className="gap-x-0.5"
>
<span className='px-0.5'>{t('datasetCreation.stepOne.button')}</span>
<RiArrowRightLine className='size-4' />
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
<RiArrowRightLine className="size-4" />
</Button>
</div>
</div>

View File

@ -25,10 +25,11 @@ const DatasourceIcon: FC<DatasourceIconProps> = ({
'flex items-center justify-center shadow-none',
ICON_CONTAINER_CLASSNAME_SIZE_MAP[size],
className,
)}
)
}
>
<div
className='h-full w-full shrink-0 rounded-md bg-cover bg-center'
className="h-full w-full shrink-0 rounded-md bg-cover bg-center"
style={{
backgroundImage: `url(${iconUrl})`,
}}

View File

@ -1,14 +1,15 @@
import { useMemo } from 'react'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import { basePath } from '@/utils/var'
import { useDataSourceList } from '@/service/use-pipeline'
import { useMemo } from 'react'
import { transformDataSourceToTool } from '@/app/components/workflow/block-selector/utils'
import { useDataSourceList } from '@/service/use-pipeline'
import { basePath } from '@/utils/var'
export const useDatasourceIcon = (data: DataSourceNodeType) => {
const { data: dataSourceListData, isSuccess } = useDataSourceList(true)
const datasourceIcon = useMemo(() => {
if (!isSuccess) return
if (!isSuccess)
return
const dataSourceList = [...(dataSourceListData || [])]
dataSourceList.forEach((item) => {
const icon = item.declaration.identity.icon

View File

@ -1,14 +1,14 @@
import { act, fireEvent, render, screen } from '@testing-library/react'
import { renderHook } from '@testing-library/react'
import type { Datasource } from '@/app/components/rag-pipeline/components/panel/test-run/types'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import type { Node } from '@/app/components/workflow/types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, fireEvent, render, renderHook, screen } from '@testing-library/react'
import React from 'react'
import DataSourceOptions from './index'
import OptionCard from './option-card'
import { BlockEnum } from '@/app/components/workflow/types'
import DatasourceIcon from './datasource-icon'
import { useDatasourceIcon } from './hooks'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import { BlockEnum, type Node } from '@/app/components/workflow/types'
import type { Datasource } from '@/app/components/rag-pipeline/components/panel/test-run/types'
import DataSourceOptions from './index'
import OptionCard from './option-card'
// ==========================================
// Mock External Dependencies
@ -75,8 +75,7 @@ const createMockPipelineNodes = (count = 3): Node<DataSourceNodeType>[] => {
plugin_id: `plugin-${i + 1}`,
datasource_name: `datasource-${i + 1}`,
}),
}),
)
}))
}
const createMockDatasourceOption = (

View File

@ -1,9 +1,9 @@
import type { Datasource } from '@/app/components/rag-pipeline/components/panel/test-run/types'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import type { Node } from '@/app/components/workflow/types'
import { useCallback, useEffect } from 'react'
import { useDatasourceOptions } from '../hooks'
import OptionCard from './option-card'
import type { Datasource } from '@/app/components/rag-pipeline/components/panel/test-run/types'
import type { Node } from '@/app/components/workflow/types'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
type DataSourceOptionsProps = {
pipelineNodes: Node<DataSourceNodeType>[]
@ -35,7 +35,7 @@ const DataSourceOptions = ({
}, [])
return (
<div className='grid w-full grid-cols-4 gap-1'>
<div className="grid w-full grid-cols-4 gap-1">
{options.map(option => (
<OptionCard
key={option.value}

View File

@ -1,6 +1,6 @@
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import React from 'react'
import { cn } from '@/utils/classnames'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import DatasourceIcon from './datasource-icon'
import { useDatasourceIcon } from './hooks'
@ -29,7 +29,7 @@ const OptionCard = ({
)}
onClick={onClick}
>
<div className='flex size-8 shrink-0 items-center justify-center rounded-lg border-[0.5px] border-components-panel-border bg-background-default-dodge p-1.5'>
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg border-[0.5px] border-components-panel-border bg-background-default-dodge p-1.5">
<DatasourceIcon iconUrl={iconUrl} />
</div>
<div

View File

@ -1,8 +1,8 @@
import type { CredentialSelectorProps } from './index'
import type { DataSourceCredential } from '@/types/pipeline'
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import React from 'react'
import CredentialSelector from './index'
import type { CredentialSelectorProps } from './index'
import type { DataSourceCredential } from '@/types/pipeline'
// Mock CredentialTypeEnum to avoid deep import chain issues
enum MockCredentialTypeEnum {
@ -80,8 +80,7 @@ const createMockCredentials = (count: number = 3): DataSourceCredential[] =>
name: `Credential ${i + 1}`,
avatar_url: `https://example.com/avatar-${i + 1}.png`,
is_default: i === 0,
}),
)
}))
const createDefaultProps = (overrides?: Partial<CredentialSelectorProps>): CredentialSelectorProps => ({
currentCredentialId: 'cred-1',

View File

@ -1,13 +1,13 @@
import type { DataSourceCredential } from '@/types/pipeline'
import { useBoolean } from 'ahooks'
import React, { useCallback, useEffect, useMemo } from 'react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import type { DataSourceCredential } from '@/types/pipeline'
import { useBoolean } from 'ahooks'
import Trigger from './trigger'
import List from './list'
import Trigger from './trigger'
export type CredentialSelectorProps = {
currentCredentialId: string
@ -40,18 +40,18 @@ const CredentialSelector = ({
<PortalToFollowElem
open={open}
onOpenChange={toggle}
placement='bottom-start'
placement="bottom-start"
offset={{
mainAxis: 4,
}}
>
<PortalToFollowElemTrigger onClick={toggle} className='grow overflow-hidden'>
<PortalToFollowElemTrigger onClick={toggle} className="grow overflow-hidden">
<Trigger
currentCredential={currentCredential}
isOpen={open}
/>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-10'>
<PortalToFollowElemContent className="z-10">
<List
currentCredentialId={currentCredentialId}
credentials={credentials}

View File

@ -1,7 +1,7 @@
import { CredentialIcon } from '@/app/components/datasets/common/credential-icon'
import type { DataSourceCredential } from '@/types/pipeline'
import { RiCheckLine } from '@remixicon/react'
import React, { useCallback } from 'react'
import { CredentialIcon } from '@/app/components/datasets/common/credential-icon'
type ItemProps = {
credential: DataSourceCredential
@ -22,7 +22,7 @@ const Item = ({
return (
<div
className='flex cursor-pointer items-center gap-x-2 rounded-lg p-2 hover:bg-state-base-hover'
className="flex cursor-pointer items-center gap-x-2 rounded-lg p-2 hover:bg-state-base-hover"
onClick={handleCredentialChange}
>
<CredentialIcon
@ -30,12 +30,12 @@ const Item = ({
name={name}
size={20}
/>
<span className='system-sm-medium grow truncate text-text-secondary'>
<span className="system-sm-medium grow truncate text-text-secondary">
{name}
</span>
{
isSelected && (
<RiCheckLine className='size-4 shrink-0 text-text-accent' />
<RiCheckLine className="size-4 shrink-0 text-text-accent" />
)
}
</div>

View File

@ -14,7 +14,7 @@ const List = ({
onCredentialChange,
}: ListProps) => {
return (
<div className='flex w-[280px] flex-col gap-y-1 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]'>
<div className="flex w-[280px] flex-col gap-y-1 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]">
{
credentials.map((credential) => {
const isSelected = credential.id === currentCredentialId

View File

@ -1,8 +1,8 @@
import React from 'react'
import type { DataSourceCredential } from '@/types/pipeline'
import { RiArrowDownSLine } from '@remixicon/react'
import { cn } from '@/utils/classnames'
import React from 'react'
import { CredentialIcon } from '@/app/components/datasets/common/credential-icon'
import { cn } from '@/utils/classnames'
type TriggerProps = {
currentCredential: DataSourceCredential | undefined
@ -30,11 +30,11 @@ const Trigger = ({
name={name}
size={20}
/>
<div className='flex grow items-center gap-x-1 overflow-hidden'>
<span className='system-md-semibold grow truncate text-text-secondary'>
<div className="flex grow items-center gap-x-1 overflow-hidden">
<span className="system-md-semibold grow truncate text-text-secondary">
{name}
</span>
<RiArrowDownSLine className='size-4 shrink-0 text-text-secondary' />
<RiArrowDownSLine className="size-4 shrink-0 text-text-secondary" />
</div>
</div>
)

View File

@ -1,7 +1,7 @@
import type { DataSourceCredential } from '@/types/pipeline'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import Header from './header'
import type { DataSourceCredential } from '@/types/pipeline'
// Mock CredentialTypeEnum to avoid deep import chain issues
enum MockCredentialTypeEnum {
@ -74,8 +74,7 @@ const createMockCredentials = (count: number = 3): DataSourceCredential[] =>
name: `Credential ${i + 1}`,
avatar_url: `https://example.com/avatar-${i + 1}.png`,
is_default: i === 0,
}),
)
}))
type HeaderProps = React.ComponentProps<typeof Header>

View File

@ -1,11 +1,11 @@
import React from 'react'
import Divider from '@/app/components/base/divider'
import Button from '@/app/components/base/button'
import { RiBookOpenLine, RiEqualizer2Line } from '@remixicon/react'
import type { CredentialSelectorProps } from './credential-selector'
import CredentialSelector from './credential-selector'
import Tooltip from '@/app/components/base/tooltip'
import { RiBookOpenLine, RiEqualizer2Line } from '@remixicon/react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import Tooltip from '@/app/components/base/tooltip'
import CredentialSelector from './credential-selector'
type HeaderProps = {
docTitle: string
@ -24,35 +24,35 @@ const Header = ({
const { t } = useTranslation()
return (
<div className='flex items-center justify-between gap-x-2'>
<div className='flex items-center gap-x-1 overflow-hidden'>
<div className="flex items-center justify-between gap-x-2">
<div className="flex items-center gap-x-1 overflow-hidden">
<CredentialSelector
{...rest}
/>
<Divider type='vertical' className='mx-1 h-3.5 shrink-0' />
<Divider type="vertical" className="mx-1 h-3.5 shrink-0" />
<Tooltip
popupContent={t('datasetPipeline.configurationTip', { pluginName })}
position='top'
position="top"
>
<Button
variant='ghost'
size='small'
className='size-6 shrink-0 px-1'
variant="ghost"
size="small"
className="size-6 shrink-0 px-1"
>
<RiEqualizer2Line
className='h-4 w-4'
className="h-4 w-4"
onClick={onClickConfiguration}
/>
</Button>
</Tooltip>
</div>
<a
className='system-xs-medium flex shrink-0 items-center gap-x-1 text-text-accent'
className="system-xs-medium flex shrink-0 items-center gap-x-1 text-text-accent"
href={docLink}
target='_blank'
rel='noopener noreferrer'
target="_blank"
rel="noopener noreferrer"
>
<RiBookOpenLine className='size-3.5 shrink-0' />
<RiBookOpenLine className="size-3.5 shrink-0" />
<span title={docTitle}>{docTitle}</span>
</a>
</div>

View File

@ -1,23 +1,23 @@
'use client'
import type { CustomFile as File, FileItem } from '@/models/datasets'
import { RiDeleteBinLine, RiErrorWarningFill, RiUploadCloud2Line } from '@remixicon/react'
import { produce } from 'immer'
import dynamic from 'next/dynamic'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { RiDeleteBinLine, RiErrorWarningFill, RiUploadCloud2Line } from '@remixicon/react'
import DocumentFileIcon from '@/app/components/datasets/common/document-file-icon'
import { cn } from '@/utils/classnames'
import type { CustomFile as File, FileItem } from '@/models/datasets'
import { ToastContext } from '@/app/components/base/toast'
import { upload } from '@/service/base'
import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils'
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n-config/language'
import { ToastContext } from '@/app/components/base/toast'
import DocumentFileIcon from '@/app/components/datasets/common/document-file-icon'
import { IS_CE_EDITION } from '@/config'
import { Theme } from '@/types/app'
import I18n from '@/context/i18n'
import useTheme from '@/hooks/use-theme'
import { LanguagesSupported } from '@/i18n-config/language'
import { upload } from '@/service/base'
import { useFileUploadConfig } from '@/service/use-common'
import { Theme } from '@/types/app'
import { cn } from '@/utils/classnames'
import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store'
import { produce } from 'immer'
import dynamic from 'next/dynamic'
const SimplePieChart = dynamic(() => import('@/app/components/base/simple-pie-chart'), { ssr: false })
@ -273,13 +273,13 @@ const LocalFile = ({
}, [handleDrop])
return (
<div className='flex flex-col'>
<div className="flex flex-col">
{!hideUpload && (
<input
ref={fileUploader}
id='fileUploader'
className='hidden'
type='file'
id="fileUploader"
className="hidden"
type="file"
multiple={supportBatchUpload}
accept={ACCEPTS.join(',')}
onChange={fileChangeHandle}
@ -291,28 +291,31 @@ const LocalFile = ({
className={cn(
'relative box-border flex min-h-20 flex-col items-center justify-center gap-1 rounded-xl border border-dashed border-components-dropzone-border bg-components-dropzone-bg px-4 py-3 text-xs leading-4 text-text-tertiary',
dragging && 'border-components-dropzone-border-accent bg-components-dropzone-bg-accent',
)}>
<div className='flex min-h-5 items-center justify-center text-sm leading-4 text-text-secondary'>
<RiUploadCloud2Line className='mr-2 size-5' />
)}
>
<div className="flex min-h-5 items-center justify-center text-sm leading-4 text-text-secondary">
<RiUploadCloud2Line className="mr-2 size-5" />
<span>
{supportBatchUpload ? t('datasetCreation.stepOne.uploader.button') : t('datasetCreation.stepOne.uploader.buttonSingleFile')}
{allowedExtensions.length > 0 && (
<label className='ml-1 cursor-pointer text-text-accent' onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.browse')}</label>
<label className="ml-1 cursor-pointer text-text-accent" onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.browse')}</label>
)}
</span>
</div>
<div>{t('datasetCreation.stepOne.uploader.tip', {
size: fileUploadConfig.file_size_limit,
supportTypes: supportTypesShowNames,
batchCount: fileUploadConfig.batch_count_limit,
totalCount: fileUploadConfig.file_upload_limit,
})}</div>
{dragging && <div ref={dragRef} className='absolute left-0 top-0 h-full w-full' />}
<div>
{t('datasetCreation.stepOne.uploader.tip', {
size: fileUploadConfig.file_size_limit,
supportTypes: supportTypesShowNames,
batchCount: fileUploadConfig.batch_count_limit,
totalCount: fileUploadConfig.file_upload_limit,
})}
</div>
{dragging && <div ref={dragRef} className="absolute left-0 top-0 h-full w-full" />}
</div>
)}
{localFileList.length > 0 && (
<div className='mt-1 flex flex-col gap-y-1'>
<div className="mt-1 flex flex-col gap-y-1">
{localFileList.map((fileItem, index) => {
const isUploading = fileItem.progress >= 0 && fileItem.progress < 100
const isError = fileItem.progress === -2
@ -325,38 +328,41 @@ const LocalFile = ({
isError && 'border-state-destructive-border bg-state-destructive-hover',
)}
>
<div className='flex w-12 shrink-0 items-center justify-center'>
<div className="flex w-12 shrink-0 items-center justify-center">
<DocumentFileIcon
size='lg'
className='shrink-0'
size="lg"
className="shrink-0"
name={fileItem.file.name}
extension={getFileType(fileItem.file)}
/>
</div>
<div className='flex shrink grow flex-col gap-0.5'>
<div className='flex w-full'>
<div className='w-0 grow truncate text-xs text-text-secondary'>{fileItem.file.name}</div>
<div className="flex shrink grow flex-col gap-0.5">
<div className="flex w-full">
<div className="w-0 grow truncate text-xs text-text-secondary">{fileItem.file.name}</div>
</div>
<div className='w-full truncate text-2xs leading-3 text-text-tertiary'>
<span className='uppercase'>{getFileType(fileItem.file)}</span>
<span className='px-1 text-text-quaternary'>·</span>
<div className="w-full truncate text-2xs leading-3 text-text-tertiary">
<span className="uppercase">{getFileType(fileItem.file)}</span>
<span className="px-1 text-text-quaternary">·</span>
<span>{getFileSize(fileItem.file.size)}</span>
</div>
</div>
<div className='flex w-16 shrink-0 items-center justify-end gap-1 pr-3'>
<div className="flex w-16 shrink-0 items-center justify-end gap-1 pr-3">
{isUploading && (
<SimplePieChart percentage={fileItem.progress} stroke={chartColor} fill={chartColor} animationDuration={0} />
)}
{
isError && (
<RiErrorWarningFill className='size-4 text-text-destructive' />
<RiErrorWarningFill className="size-4 text-text-destructive" />
)
}
<span className='flex h-6 w-6 cursor-pointer items-center justify-center' onClick={(e) => {
e.stopPropagation()
removeFile(fileItem.fileID)
}}>
<RiDeleteBinLine className='size-4 text-text-tertiary' />
<span
className="flex h-6 w-6 cursor-pointer items-center justify-center"
onClick={(e) => {
e.stopPropagation()
removeFile(fileItem.fileID)
}}
>
<RiDeleteBinLine className="size-4 text-text-tertiary" />
</span>
</div>
</div>

View File

@ -1,9 +1,9 @@
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import type { DataSourceNotionWorkspace, NotionPage } from '@/models/common'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import React from 'react'
import OnlineDocuments from './index'
import type { DataSourceNotionWorkspace, NotionPage } from '@/models/common'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import { VarKindType } from '@/app/components/workflow/nodes/_base/types'
import OnlineDocuments from './index'
// ==========================================
// Mock Modules
@ -99,7 +99,7 @@ vi.mock('../base/header', () => ({
// Mock SearchInput component
vi.mock('@/app/components/base/notion-page-selector/search-input', () => ({
default: ({ value, onChange }: { value: string; onChange: (v: string) => void }) => (
default: ({ value, onChange }: { value: string, onChange: (v: string) => void }) => (
<div data-testid="search-input">
<input
data-testid="search-input-field"
@ -179,7 +179,7 @@ const createMockWorkspace = (overrides?: Partial<DataSourceNotionWorkspace>): Da
...overrides,
})
const createMockCredential = (overrides?: Partial<{ id: string; name: string }>) => ({
const createMockCredential = (overrides?: Partial<{ id: string, name: string }>) => ({
id: 'cred-1',
name: 'Test Credential',
avatar_url: 'https://example.com/avatar.png',

View File

@ -1,22 +1,22 @@
import { useCallback, useEffect, useMemo } from 'react'
import SearchInput from '@/app/components/base/notion-page-selector/search-input'
import PageSelector from './page-selector'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import type { DataSourceNotionPageMap, DataSourceNotionWorkspace } from '@/models/common'
import Header from '../base/header'
import type { DataSourceNodeCompletedResponse, DataSourceNodeErrorResponse } from '@/types/pipeline'
import { useCallback, useEffect, useMemo } from 'react'
import { useShallow } from 'zustand/react/shallow'
import Loading from '@/app/components/base/loading'
import SearchInput from '@/app/components/base/notion-page-selector/search-input'
import Toast from '@/app/components/base/toast'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useDocLink } from '@/context/i18n'
import { useModalContextSelector } from '@/context/modal-context'
import { DatasourceType } from '@/models/pipeline'
import { ssePost } from '@/service/base'
import Toast from '@/app/components/base/toast'
import type { DataSourceNodeCompletedResponse, DataSourceNodeErrorResponse } from '@/types/pipeline'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store'
import { useShallow } from 'zustand/react/shallow'
import { useModalContextSelector } from '@/context/modal-context'
import Title from './title'
import { useGetDataSourceAuth } from '@/service/use-datasource'
import Loading from '@/app/components/base/loading'
import { useDocLink } from '@/context/i18n'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import Header from '../base/header'
import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store'
import PageSelector from './page-selector'
import Title from './title'
type OnlineDocumentsProps = {
nodeId: string
@ -106,7 +106,8 @@ const OnlineDocuments = ({
}, [dataSourceStore, datasourceNodeRunURL, nodeData.datasource_parameters])
useEffect(() => {
if (!currentCredentialId) return
if (!currentCredentialId)
return
getOnlineDocuments()
}, [currentCredentialId])
@ -134,9 +135,9 @@ const OnlineDocuments = ({
}, [setShowAccountSettingModal])
return (
<div className='flex flex-col gap-y-2'>
<div className="flex flex-col gap-y-2">
<Header
docTitle='Docs'
docTitle="Docs"
docLink={docLink('/guides/knowledge-base/knowledge-pipeline/authorize-data-source')}
onClickConfiguration={handleSetting}
pluginName={nodeData.datasource_label}
@ -144,9 +145,9 @@ const OnlineDocuments = ({
onCredentialChange={onCredentialChange}
credentials={dataSourceAuth?.result || []}
/>
<div className='rounded-xl border border-components-panel-border bg-background-default-subtle'>
<div className='flex items-center gap-x-2 rounded-t-xl border-b border-b-divider-regular bg-components-panel-bg p-1 pl-3'>
<div className='flex grow items-center'>
<div className="rounded-xl border border-components-panel-border bg-background-default-subtle">
<div className="flex items-center gap-x-2 rounded-t-xl border-b border-b-divider-regular bg-components-panel-bg p-1 pl-3">
<div className="flex grow items-center">
<Title name={nodeData.datasource_label} />
</div>
<SearchInput
@ -154,25 +155,27 @@ const OnlineDocuments = ({
onChange={handleSearchValueChange}
/>
</div>
<div className='overflow-hidden rounded-b-xl'>
{documentsData?.length ? (
<PageSelector
checkedIds={selectedPagesId}
disabledValue={new Set()}
searchValue={searchValue}
list={documentsData[0].pages || []}
pagesMap={PagesMapAndSelectedPagesId}
onSelect={handleSelectPages}
canPreview={!isInPipeline}
onPreview={handlePreviewPage}
isMultipleChoice={supportBatchUpload}
currentCredentialId={currentCredentialId}
/>
) : (
<div className='flex h-[296px] items-center justify-center'>
<Loading type='app' />
</div>
)}
<div className="overflow-hidden rounded-b-xl">
{documentsData?.length
? (
<PageSelector
checkedIds={selectedPagesId}
disabledValue={new Set()}
searchValue={searchValue}
list={documentsData[0].pages || []}
pagesMap={PagesMapAndSelectedPagesId}
onSelect={handleSelectPages}
canPreview={!isInPipeline}
onPreview={handlePreviewPage}
isMultipleChoice={supportBatchUpload}
currentCredentialId={currentCredentialId}
/>
)
: (
<div className="flex h-[296px] items-center justify-center">
<Loading type="app" />
</div>
)}
</div>
</div>
</div>

View File

@ -1,8 +1,8 @@
import type { NotionPageTreeItem, NotionPageTreeMap } from './index'
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import PageSelector from './index'
import type { NotionPageTreeItem, NotionPageTreeMap } from './index'
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
import { recursivePushInParentDescendants } from './utils'
// ==========================================

View File

@ -1,7 +1,7 @@
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { FixedSizeList as List } from 'react-window'
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
import Item from './item'
import { recursivePushInParentDescendants } from './utils'
@ -153,7 +153,7 @@ const PageSelector = ({
if (!currentDataList.length) {
return (
<div className='flex h-[296px] items-center justify-center text-[13px] text-text-tertiary'>
<div className="flex h-[296px] items-center justify-center text-[13px] text-text-tertiary">
{t('common.dataSource.notion.selector.noSearchResult')}
</div>
)
@ -161,11 +161,11 @@ const PageSelector = ({
return (
<List
className='py-2'
className="py-2"
height={296}
itemCount={currentDataList.length}
itemSize={28}
width='100%'
width="100%"
itemKey={(index, data) => data.dataList[index].page_id}
itemData={{
dataList: currentDataList,

View File

@ -1,13 +1,13 @@
import type { ListChildComponentProps } from 'react-window'
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { areEqual } from 'react-window'
import type { ListChildComponentProps } from 'react-window'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import Checkbox from '@/app/components/base/checkbox'
import NotionIcon from '@/app/components/base/notion-icon'
import { cn } from '@/utils/classnames'
import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common'
import Radio from '@/app/components/base/radio/ui'
import { cn } from '@/utils/classnames'
type NotionPageTreeItem = {
children: Set<string>
@ -63,14 +63,14 @@ const Item = ({ index, style, data }: ListChildComponentProps<{
if (hasChild) {
return (
<div
className='mr-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-md hover:bg-components-button-ghost-bg-hover'
className="mr-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-md hover:bg-components-button-ghost-bg-hover"
style={{ marginLeft: current.depth * 8 }}
onClick={() => handleToggle(index)}
>
{
current.expand
? <RiArrowDownSLine className='h-4 w-4 text-text-tertiary' />
: <RiArrowRightSLine className='h-4 w-4 text-text-tertiary' />
? <RiArrowDownSLine className="h-4 w-4 text-text-tertiary" />
: <RiArrowRightSLine className="h-4 w-4 text-text-tertiary" />
}
</div>
)
@ -81,42 +81,44 @@ const Item = ({ index, style, data }: ListChildComponentProps<{
)
}
return (
<div className='mr-1 h-5 w-5 shrink-0' style={{ marginLeft: current.depth * 8 }} />
<div className="mr-1 h-5 w-5 shrink-0" style={{ marginLeft: current.depth * 8 }} />
)
}
return (
<div
className={cn('group flex cursor-pointer items-center rounded-md pl-2 pr-[2px] hover:bg-state-base-hover',
previewPageId === current.page_id && 'bg-state-base-hover')}
className={cn('group flex cursor-pointer items-center rounded-md pl-2 pr-[2px] hover:bg-state-base-hover', previewPageId === current.page_id && 'bg-state-base-hover')}
style={{ ...style, top: style.top as number + 8, left: 8, right: 8, width: 'calc(100% - 16px)' }}
>
{isMultipleChoice ? (
<Checkbox
className='mr-2 shrink-0'
checked={checkedIds.has(current.page_id)}
disabled={disabled}
onCheck={() => {
handleCheck(index)
}}
/>) : (
<Radio
className='mr-2 shrink-0'
isChecked={checkedIds.has(current.page_id)}
disabled={disabled}
onCheck={() => {
handleCheck(index)
}}
/>
)}
{isMultipleChoice
? (
<Checkbox
className="mr-2 shrink-0"
checked={checkedIds.has(current.page_id)}
disabled={disabled}
onCheck={() => {
handleCheck(index)
}}
/>
)
: (
<Radio
className="mr-2 shrink-0"
isChecked={checkedIds.has(current.page_id)}
disabled={disabled}
onCheck={() => {
handleCheck(index)
}}
/>
)}
{!searchValue && renderArrow()}
<NotionIcon
className='mr-1 shrink-0'
type='page'
className="mr-1 shrink-0"
type="page"
src={current.page_icon}
/>
<div
className='grow truncate text-[13px] font-medium leading-4 text-text-secondary'
className="grow truncate text-[13px] font-medium leading-4 text-text-secondary"
title={current.page_name}
>
{current.page_name}
@ -124,10 +126,11 @@ const Item = ({ index, style, data }: ListChildComponentProps<{
{
canPreview && (
<div
className='ml-1 hidden h-6 shrink-0 cursor-pointer items-center rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-xs
className="ml-1 hidden h-6 shrink-0 cursor-pointer items-center rounded-md border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-2 text-xs
font-medium leading-4 text-components-button-secondary-text shadow-xs shadow-shadow-shadow-3 backdrop-blur-[10px]
hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover group-hover:flex'
onClick={() => handlePreview(index)}>
hover:border-components-button-secondary-border-hover hover:bg-components-button-secondary-bg-hover group-hover:flex"
onClick={() => handlePreview(index)}
>
{t('common.dataSource.notion.selector.preview')}
</div>
)
@ -135,7 +138,7 @@ const Item = ({ index, style, data }: ListChildComponentProps<{
{
searchValue && (
<div
className='ml-1 max-w-[120px] shrink-0 truncate text-xs text-text-quaternary'
className="ml-1 max-w-[120px] shrink-0 truncate text-xs text-text-quaternary"
title={breadCrumbs.join(' / ')}
>
{breadCrumbs.join(' / ')}

View File

@ -1,5 +1,5 @@
import type { DataSourceNotionPageMap } from '@/models/common'
import type { NotionPageTreeItem, NotionPageTreeMap } from './index'
import type { DataSourceNotionPageMap } from '@/models/common'
export const recursivePushInParentDescendants = (
pagesMap: DataSourceNotionPageMap,

View File

@ -11,7 +11,7 @@ const Title = ({
const { t } = useTranslation()
return (
<div className='system-sm-medium px-[5px] py-1 text-text-secondary'>
<div className="system-sm-medium px-[5px] py-1 text-text-secondary">
{t('datasetPipeline.onlineDocument.pageSelectorTitle', { name })}
</div>
)

View File

@ -1,6 +1,6 @@
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import { fireEvent, render, screen } from '@testing-library/react'
import Connect from './index'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
// ==========================================
// Mock Modules

View File

@ -1,9 +1,9 @@
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { Icon3Dots } from '@/app/components/base/icons/src/vender/line/others'
import BlockIcon from '@/app/components/workflow/block-icon'
import { useToolIcon } from '@/app/components/workflow/hooks'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import { BlockEnum } from '@/app/components/workflow/types'
type ConnectProps = {
@ -19,27 +19,27 @@ const Connect = ({
const toolIcon = useToolIcon(nodeData)
return (
<div className='flex flex-col items-start gap-y-2 rounded-xl bg-workflow-process-bg p-6'>
<div className='flex size-12 items-center justify-center rounded-[10px] border-[0.5px] border-components-card-border bg-components-card-bg p-1 shadow-lg shadow-shadow-shadow-5'>
<div className="flex flex-col items-start gap-y-2 rounded-xl bg-workflow-process-bg p-6">
<div className="flex size-12 items-center justify-center rounded-[10px] border-[0.5px] border-components-card-border bg-components-card-bg p-1 shadow-lg shadow-shadow-shadow-5">
<BlockIcon
type={BlockEnum.DataSource}
toolIcon={toolIcon}
size='md'
size="md"
/>
</div>
<div className='flex flex-col gap-y-1'>
<div className='flex flex-col gap-y-1 pb-3 pt-1'>
<div className='system-md-semibold text-text-secondary'>
<span className='relative'>
<div className="flex flex-col gap-y-1">
<div className="flex flex-col gap-y-1 pb-3 pt-1">
<div className="system-md-semibold text-text-secondary">
<span className="relative">
{t('datasetPipeline.onlineDrive.notConnected', { name: nodeData.title })}
<Icon3Dots className='absolute -right-2.5 -top-1.5 size-4 text-text-secondary' />
<Icon3Dots className="absolute -right-2.5 -top-1.5 size-4 text-text-secondary" />
</span>
</div>
<div className='system-sm-regular text-text-tertiary'>
<div className="system-sm-regular text-text-tertiary">
{t('datasetPipeline.onlineDrive.notConnectedTip', { name: nodeData.title })}
</div>
</div>
<Button className='w-fit' variant='primary' onClick={onSetting}>
<Button className="w-fit" variant="primary" onClick={onSetting}>
{t('datasetCreation.stepOne.connect')}
</Button>
</div>

View File

@ -1,7 +1,7 @@
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { BucketsGray } from '@/app/components/base/icons/src/public/knowledge/online-drive'
import Tooltip from '@/app/components/base/tooltip'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
type BucketProps = {
@ -33,16 +33,16 @@ const Bucket = ({
popupContent={t('datasetPipeline.onlineDrive.breadcrumbs.allBuckets')}
>
<button
type='button'
className='flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover'
type="button"
className="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded-md hover:bg-state-base-hover"
onClick={handleBackToBucketList}
>
<BucketsGray />
</button>
</Tooltip>
<span className='system-xs-regular text-divider-deep'>/</span>
<span className="system-xs-regular text-divider-deep">/</span>
<button
type='button'
type="button"
className={cn(
'max-w-full shrink truncate rounded-md px-[5px] py-1',
isActive ? 'system-sm-medium text-text-secondary' : 'system-sm-regular text-text-tertiary',
@ -54,7 +54,7 @@ const Bucket = ({
>
{bucketName}
</button>
{showSeparator && <span className='system-xs-regular shrink-0 text-divider-deep'>/</span>}
{showSeparator && <span className="system-xs-regular shrink-0 text-divider-deep">/</span>}
</>
)
}

View File

@ -1,6 +1,6 @@
import React from 'react'
import { cn } from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
type DriveProps = {
breadcrumbs: string[]
@ -16,7 +16,7 @@ const Drive = ({
return (
<>
<button
type='button'
type="button"
className={cn(
'max-w-full shrink truncate rounded-md px-[5px] py-1',
breadcrumbs.length > 0 && 'system-sm-regular text-text-tertiary hover:bg-state-base-hover',
@ -27,7 +27,7 @@ const Drive = ({
>
{t('datasetPipeline.onlineDrive.breadcrumbs.allFiles')}
</button>
{breadcrumbs.length > 0 && <span className='system-xs-regular text-divider-deep'>/</span>}
{breadcrumbs.length > 0 && <span className="system-xs-regular text-divider-deep">/</span>}
</>
)
}

View File

@ -528,10 +528,13 @@ describe('Dropdown', () => {
fireEvent.click(screen.getByText('folder'))
// Rerender with different callback
rerender(<Dropdown {...createDefaultProps({
breadcrumbs: ['folder'],
onBreadcrumbClick: mockOnBreadcrumbClick2,
})} />)
rerender(
<Dropdown {...createDefaultProps({
breadcrumbs: ['folder'],
onBreadcrumbClick: mockOnBreadcrumbClick2,
})}
/>,
)
// Open and click with second callback
fireEvent.click(screen.getByRole('button'))

View File

@ -1,10 +1,10 @@
import { RiMoreFill } from '@remixicon/react'
import React, { useCallback, useState } from 'react'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { RiMoreFill } from '@remixicon/react'
import { cn } from '@/utils/classnames'
import Menu from './menu'
@ -34,7 +34,7 @@ const Dropdown = ({
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-start'
placement="bottom-start"
offset={{
mainAxis: 4,
crossAxis: -13,
@ -42,23 +42,23 @@ const Dropdown = ({
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
<button
type='button'
type="button"
className={cn(
'flex size-6 items-center justify-center rounded-md',
open ? 'bg-state-base-hover' : 'hover:bg-state-base-hover',
)}
>
<RiMoreFill className='size-4 text-text-tertiary' />
<RiMoreFill className="size-4 text-text-tertiary" />
</button>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-[11]'>
<PortalToFollowElemContent className="z-[11]">
<Menu
breadcrumbs={breadcrumbs}
startIndex={startIndex}
onBreadcrumbClick={handleBreadCrumbClick}
/>
</PortalToFollowElemContent>
<span className='system-xs-regular text-divider-deep'>/</span>
<span className="system-xs-regular text-divider-deep">/</span>
</PortalToFollowElem>
)
}

View File

@ -17,7 +17,7 @@ const Item = ({
return (
<div
className='system-md-regular rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'
className="system-md-regular rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover"
onClick={handleClick}
>
{name}

View File

@ -13,7 +13,7 @@ const Menu = ({
onBreadcrumbClick,
}: MenuProps) => {
return (
<div className='flex w-[136px] flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]'>
<div className="flex w-[136px] flex-col rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]">
{breadcrumbs.map((breadcrumb, index) => {
return (
<Item

View File

@ -2,9 +2,9 @@ import React, { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useDataSourceStore, useDataSourceStoreWithSelector } from '../../../../store'
import Bucket from './bucket'
import BreadcrumbItem from './item'
import Dropdown from './dropdown'
import Drive from './drive'
import Dropdown from './dropdown'
import BreadcrumbItem from './item'
type BreadcrumbsProps = {
breadcrumbs: string[]
@ -80,9 +80,9 @@ const Breadcrumbs = ({
}, [dataSourceStore])
return (
<div className='flex grow items-center overflow-hidden'>
<div className="flex grow items-center overflow-hidden">
{showSearchResult && (
<div className='system-sm-medium text-test-secondary px-[5px]'>
<div className="system-sm-medium text-test-secondary px-[5px]">
{t('datasetPipeline.onlineDrive.breadcrumbs.searchResult', {
searchResultsLength,
folderName: breadcrumbs.length > 0 ? breadcrumbs[breadcrumbs.length - 1] : bucket,
@ -90,12 +90,12 @@ const Breadcrumbs = ({
</div>
)}
{!showSearchResult && showBucketListTitle && (
<div className='system-sm-medium text-test-secondary px-[5px]'>
<div className="system-sm-medium text-test-secondary px-[5px]">
{t('datasetPipeline.onlineDrive.breadcrumbs.allBuckets')}
</div>
)}
{!showSearchResult && !showBucketListTitle && (
<div className='flex w-full items-center gap-x-0.5 overflow-hidden'>
<div className="flex w-full items-center gap-x-0.5 overflow-hidden">
{hasBucket && bucket && (
<Bucket
bucketName={bucket}

View File

@ -26,7 +26,7 @@ const BreadcrumbItem = ({
return (
<>
<button
type='button'
type="button"
className={cn(
'max-w-full shrink truncate rounded-md px-[5px] py-1',
isActive ? 'system-sm-medium text-text-secondary' : 'system-sm-regular text-text-tertiary',
@ -38,7 +38,7 @@ const BreadcrumbItem = ({
>
{name}
</button>
{showSeparator && <span className='system-xs-regular shrink-0 text-divider-deep'>/</span>}
{showSeparator && <span className="system-xs-regular shrink-0 text-divider-deep">/</span>}
</>
)
}

View File

@ -1,7 +1,7 @@
import React from 'react'
import Breadcrumbs from './breadcrumbs'
import Input from '@/app/components/base/input'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import Breadcrumbs from './breadcrumbs'
type HeaderProps = {
breadcrumbs: string[]
@ -27,7 +27,7 @@ const Header = ({
const { t } = useTranslation()
return (
<div className='flex items-center gap-x-2 bg-components-panel-bg p-1 pl-3'>
<div className="flex items-center gap-x-2 bg-components-panel-bg p-1 pl-3">
<Breadcrumbs
breadcrumbs={breadcrumbs}
keywords={keywords}
@ -42,7 +42,7 @@ const Header = ({
placeholder={t('datasetPipeline.onlineDrive.breadcrumbs.searchPlaceholder')}
showLeftIcon
showClearIcon
wrapperClassName='w-[200px] h-8 shrink-0'
wrapperClassName="w-[200px] h-8 shrink-0"
/>
</div>
)

View File

@ -1,8 +1,8 @@
import type { OnlineDriveFile } from '@/models/pipeline'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import FileList from './index'
import type { OnlineDriveFile } from '@/models/pipeline'
import { OnlineDriveFileType } from '@/models/pipeline'
import FileList from './index'
// ==========================================
// Mock Modules
@ -544,8 +544,7 @@ describe('FileList', () => {
it('should handle large number of files', () => {
// Arrange
const fileList = Array.from({ length: 50 }, (_, i) =>
createMockOnlineDriveFile({ id: `file-${i}`, name: `file-${i}.txt` }),
)
createMockOnlineDriveFile({ id: `file-${i}`, name: `file-${i}.txt` }))
const props = createDefaultProps({ fileList })
// Act
@ -598,8 +597,7 @@ describe('FileList', () => {
])('should handle $description correctly', ({ isLoading, fileCount }) => {
// Arrange
const fileList = Array.from({ length: fileCount }, (_, i) =>
createMockOnlineDriveFile({ id: `file-${i}`, name: `file-${i}.txt` }),
)
createMockOnlineDriveFile({ id: `file-${i}`, name: `file-${i}.txt` }))
const props = createDefaultProps({ isLoading, fileList })
// Act

View File

@ -1,8 +1,8 @@
import type { OnlineDriveFile } from '@/models/pipeline'
import { useDebounceFn } from 'ahooks'
import { useState } from 'react'
import Header from './header'
import List from './list'
import { useState } from 'react'
import { useDebounceFn } from 'ahooks'
type FileListProps = {
fileList: OnlineDriveFile[]
@ -56,7 +56,7 @@ const FileList = ({
}
return (
<div className='flex h-[400px] flex-col overflow-hidden rounded-xl border border-components-panel-border bg-components-panel-bg shadow-xs shadow-shadow-shadow-3'>
<div className="flex h-[400px] flex-col overflow-hidden rounded-xl border border-components-panel-border bg-components-panel-bg shadow-xs shadow-shadow-shadow-3">
<Header
breadcrumbs={breadcrumbs}
inputValue={inputValue}

View File

@ -5,8 +5,8 @@ const EmptyFolder = () => {
const { t } = useTranslation()
return (
<div className='flex size-full items-center justify-center rounded-[10px] bg-background-section px-1 py-1.5'>
<span className='system-xs-regular text-text-tertiary'>{t('datasetPipeline.onlineDrive.emptyFolder')}</span>
<div className="flex size-full items-center justify-center rounded-[10px] bg-background-section px-1 py-1.5">
<span className="system-xs-regular text-text-tertiary">{t('datasetPipeline.onlineDrive.emptyFolder')}</span>
</div>
)
}

View File

@ -1,7 +1,7 @@
import Button from '@/app/components/base/button'
import { SearchMenu } from '@/app/components/base/icons/src/vender/knowledge'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { SearchMenu } from '@/app/components/base/icons/src/vender/knowledge'
type EmptySearchResultProps = {
onResetKeywords: () => void
@ -15,18 +15,18 @@ const EmptySearchResult = ({
const { t } = useTranslation()
return (
<div className='flex size-full flex-col items-center justify-center gap-y-2 rounded-[10px] bg-background-section p-6'>
<SearchMenu className='size-8 text-text-tertiary' />
<div className='system-sm-regular text-text-secondary'>
<div className="flex size-full flex-col items-center justify-center gap-y-2 rounded-[10px] bg-background-section p-6">
<SearchMenu className="size-8 text-text-tertiary" />
<div className="system-sm-regular text-text-secondary">
{t('datasetPipeline.onlineDrive.emptySearchResult')}
</div>
<Button
variant='secondary-accent'
size='small'
variant="secondary-accent"
size="small"
onClick={onResetKeywords}
className='px-1.5'
className="px-1.5"
>
<span className='px-[3px]'>{t('datasetPipeline.onlineDrive.resetKeywords')}</span>
<span className="px-[3px]">{t('datasetPipeline.onlineDrive.resetKeywords')}</span>
</Button>
</div>
)

View File

@ -1,9 +1,9 @@
import React, { useMemo } from 'react'
import { OnlineDriveFileType } from '@/models/pipeline'
import { BucketsBlue, Folder } from '@/app/components/base/icons/src/public/knowledge/online-drive'
import FileTypeIcon from '@/app/components/base/file-uploader/file-type-icon'
import { getFileType } from './utils'
import { BucketsBlue, Folder } from '@/app/components/base/icons/src/public/knowledge/online-drive'
import { OnlineDriveFileType } from '@/models/pipeline'
import { cn } from '@/utils/classnames'
import { getFileType } from './utils'
type FileIconProps = {
type: OnlineDriveFileType

View File

@ -1,9 +1,9 @@
import type { Mock } from 'vitest'
import type { OnlineDriveFile } from '@/models/pipeline'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import React from 'react'
import List from './index'
import type { OnlineDriveFile } from '@/models/pipeline'
import { OnlineDriveFileType } from '@/models/pipeline'
import List from './index'
// ==========================================
// Mock Modules
@ -1086,35 +1086,34 @@ describe('List', () => {
{ isLoading: false, fileCount: 0, keywords: '', expectedState: 'empty-folder' },
{ isLoading: false, fileCount: 0, keywords: 'search', expectedState: 'empty-search' },
{ isLoading: false, fileCount: 5, keywords: '', expectedState: 'file-list' },
])('should render $expectedState when isLoading=$isLoading, fileCount=$fileCount, keywords=$keywords',
({ isLoading, fileCount, keywords, expectedState }) => {
// Arrange
const fileList = createMockFileList(fileCount)
const props = createDefaultProps({ fileList, isLoading, keywords })
])('should render $expectedState when isLoading=$isLoading, fileCount=$fileCount, keywords=$keywords', ({ isLoading, fileCount, keywords, expectedState }) => {
// Arrange
const fileList = createMockFileList(fileCount)
const props = createDefaultProps({ fileList, isLoading, keywords })
// Act
render(<List {...props} />)
// Act
render(<List {...props} />)
// Assert
switch (expectedState) {
case 'all-loading':
expect(screen.getByRole('status')).toBeInTheDocument()
break
case 'partial-loading':
expect(screen.getByRole('status')).toBeInTheDocument()
expect(screen.getByTestId('item-file-1')).toBeInTheDocument()
break
case 'empty-folder':
expect(screen.getByTestId('empty-folder')).toBeInTheDocument()
break
case 'empty-search':
expect(screen.getByTestId('empty-search-result')).toBeInTheDocument()
break
case 'file-list':
expect(screen.getByTestId('item-file-1')).toBeInTheDocument()
break
}
})
// Assert
switch (expectedState) {
case 'all-loading':
expect(screen.getByRole('status')).toBeInTheDocument()
break
case 'partial-loading':
expect(screen.getByRole('status')).toBeInTheDocument()
expect(screen.getByTestId('item-file-1')).toBeInTheDocument()
break
case 'empty-folder':
expect(screen.getByTestId('empty-folder')).toBeInTheDocument()
break
case 'empty-search':
expect(screen.getByTestId('empty-search-result')).toBeInTheDocument()
break
case 'file-list':
expect(screen.getByTestId('item-file-1')).toBeInTheDocument()
break
}
})
it.each([
{ selectedCount: 0, expectedSelected: [] },
@ -1298,7 +1297,7 @@ describe('EmptySearchResult', () => {
// ==========================================
describe('FileIcon', () => {
// Get real component for testing
type FileIconProps = { type: OnlineDriveFileType; fileName: string; size?: 'sm' | 'md' | 'lg' | 'xl'; className?: string }
type FileIconProps = { type: OnlineDriveFileType, fileName: string, size?: 'sm' | 'md' | 'lg' | 'xl', className?: string }
let ActualFileIcon: React.ComponentType<FileIconProps>
beforeAll(async () => {
@ -1757,7 +1756,7 @@ describe('utils', () => {
let FileAppearanceTypeEnum: Record<string, string>
beforeAll(async () => {
const utils = await vi.importActual<{ getFileExtension: typeof getFileExtension; getFileType: typeof getFileType }>('./utils')
const utils = await vi.importActual<{ getFileExtension: typeof getFileExtension, getFileType: typeof getFileType }>('./utils')
const types = await vi.importActual<{ FileAppearanceTypeEnum: typeof FileAppearanceTypeEnum }>('@/app/components/base/file-uploader/types')
getFileExtension = utils.getFileExtension
getFileType = utils.getFileType

View File

@ -1,12 +1,12 @@
import type { OnlineDriveFile } from '@/models/pipeline'
import { RiLoader2Line } from '@remixicon/react'
import React, { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import type { OnlineDriveFile } from '@/models/pipeline'
import Item from './item'
import Loading from '@/app/components/base/loading'
import { useDataSourceStore } from '../../../store'
import EmptyFolder from './empty-folder'
import EmptySearchResult from './empty-search-result'
import Loading from '@/app/components/base/loading'
import { RiLoader2Line } from '@remixicon/react'
import { useDataSourceStore } from '../../../store'
import Item from './item'
type FileListProps = {
fileList: OnlineDriveFile[]
@ -54,10 +54,10 @@ const List = ({
const isSearchResultEmpty = !isLoading && fileList.length === 0 && keywords.length > 0
return (
<div className='grow overflow-hidden p-1 pt-0'>
<div className="grow overflow-hidden p-1 pt-0">
{
isAllLoading && (
<Loading type='app' />
<Loading type="app" />
)
}
{
@ -71,7 +71,7 @@ const List = ({
)
}
{fileList.length > 0 && (
<div className='flex h-full flex-col gap-y-px overflow-y-auto rounded-[10px] bg-background-section px-1 py-1.5'>
<div className="flex h-full flex-col gap-y-px overflow-y-auto rounded-[10px] bg-background-section px-1 py-1.5">
{
fileList.map((file) => {
const isSelected = selectedFileIds.includes(file.id)
@ -90,16 +90,16 @@ const List = ({
{
isPartialLoading && (
<div
className='flex items-center justify-center py-2'
role='status'
aria-live='polite'
className="flex items-center justify-center py-2"
role="status"
aria-live="polite"
aria-label={t('appApi.loading')}
>
<RiLoader2Line className='animation-spin size-4 text-text-tertiary' />
<RiLoader2Line className="animation-spin size-4 text-text-tertiary" />
</div>
)
}
<div ref={anchorRef} className='h-0' />
<div ref={anchorRef} className="h-0" />
</div>
)}
</div>

View File

@ -1,13 +1,13 @@
import Checkbox from '@/app/components/base/checkbox'
import Radio from '@/app/components/base/radio/ui'
import type { Placement } from '@floating-ui/react'
import type { OnlineDriveFile } from '@/models/pipeline'
import React, { useCallback } from 'react'
import FileIcon from './file-icon'
import { formatFileSize } from '@/utils/format'
import Tooltip from '@/app/components/base/tooltip'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import Radio from '@/app/components/base/radio/ui'
import Tooltip from '@/app/components/base/tooltip'
import { cn } from '@/utils/classnames'
import type { Placement } from '@floating-ui/react'
import { formatFileSize } from '@/utils/format'
import FileIcon from './file-icon'
type ItemProps = {
file: OnlineDriveFile
@ -33,11 +33,13 @@ const Item = ({
const isFolder = type === 'folder'
const Wrapper = disabled ? Tooltip : React.Fragment
const wrapperProps = disabled ? {
popupContent: t('datasetPipeline.onlineDrive.notSupportedFileType'),
position: 'top-end' as Placement,
offset: { mainAxis: 4, crossAxis: -104 },
} : {}
const wrapperProps = disabled
? {
popupContent: t('datasetPipeline.onlineDrive.notSupportedFileType'),
position: 'top-end' as Placement,
offset: { mainAxis: 4, crossAxis: -104 },
}
: {}
const handleSelect = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
@ -46,7 +48,8 @@ const Item = ({
const handleClickItem = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
if (disabled) return
if (disabled)
return
if (isBucket || isFolder) {
onOpen(file)
return
@ -56,12 +59,12 @@ const Item = ({
return (
<div
className='flex cursor-pointer items-center gap-2 rounded-md px-2 py-[3px] hover:bg-state-base-hover'
className="flex cursor-pointer items-center gap-2 rounded-md px-2 py-[3px] hover:bg-state-base-hover"
onClick={handleClickItem}
>
{!isBucket && isMultipleChoice && (
<Checkbox
className='shrink-0'
className="shrink-0"
disabled={disabled}
id={id}
checked={isSelected}
@ -70,7 +73,7 @@ const Item = ({
)}
{!isBucket && !isMultipleChoice && (
<Radio
className='shrink-0'
className="shrink-0"
disabled={disabled}
isChecked={isSelected}
onCheck={handleSelect}
@ -83,16 +86,17 @@ const Item = ({
className={cn(
'flex grow items-center gap-x-1 overflow-hidden py-0.5',
disabled && 'opacity-30',
)}>
<FileIcon type={type} fileName={name} className='shrink-0 transform-gpu' />
)}
>
<FileIcon type={type} fileName={name} className="shrink-0 transform-gpu" />
<span
className='system-sm-medium grow truncate text-text-secondary'
className="system-sm-medium grow truncate text-text-secondary"
title={name}
>
{name}
</span>
{!isFolder && typeof size === 'number' && (
<span className='system-xs-regular shrink-0 text-text-tertiary'>{formatFileSize(size)}</span>
<span className="system-xs-regular shrink-0 text-text-tertiary">{formatFileSize(size)}</span>
)}
</div>
</Wrapper>

View File

@ -1,7 +1,7 @@
import { RiBookOpenLine, RiEqualizer2Line } from '@remixicon/react'
import React from 'react'
import Button from '@/app/components/base/button'
import Divider from '@/app/components/base/divider'
import { RiBookOpenLine, RiEqualizer2Line } from '@remixicon/react'
type HeaderProps = {
onClickConfiguration?: () => void
@ -15,31 +15,31 @@ const Header = ({
docLink,
}: HeaderProps) => {
return (
<div className='flex items-center gap-x-2'>
<div className='flex shrink-0 grow items-center gap-x-1'>
<div className='w-20 bg-black'>
<div className="flex items-center gap-x-2">
<div className="flex shrink-0 grow items-center gap-x-1">
<div className="w-20 bg-black">
{/* placeholder */}
</div>
<Divider type='vertical' className='mx-1 h-3.5' />
<Divider type="vertical" className="mx-1 h-3.5" />
<Button
variant='ghost'
size='small'
className='px-1'
variant="ghost"
size="small"
className="px-1"
>
<RiEqualizer2Line
className='size-4'
className="size-4"
onClick={onClickConfiguration}
/>
</Button>
</div>
<a
className='system-xs-medium flex items-center gap-x-1 overflow-hidden text-text-accent'
className="system-xs-medium flex items-center gap-x-1 overflow-hidden text-text-accent"
href={docLink}
target='_blank'
rel='noopener noreferrer'
target="_blank"
rel="noopener noreferrer"
>
<RiBookOpenLine className='size-3.5 shrink-0' />
<span className='grow truncate' title={docTitle}>{docTitle}</span>
<RiBookOpenLine className="size-3.5 shrink-0" />
<span className="grow truncate" title={docTitle}>{docTitle}</span>
</a>
</div>
)

View File

@ -1,13 +1,13 @@
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import type { OnlineDriveFile } from '@/models/pipeline'
import type { OnlineDriveData } from '@/types/pipeline'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import React from 'react'
import OnlineDrive from './index'
import Header from './header'
import { convertOnlineDriveData, isBucketListInitiation, isFile } from './utils'
import type { OnlineDriveFile } from '@/models/pipeline'
import { DatasourceType, OnlineDriveFileType } from '@/models/pipeline'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import type { OnlineDriveData } from '@/types/pipeline'
import { DatasourceType, OnlineDriveFileType } from '@/models/pipeline'
import Header from './header'
import OnlineDrive from './index'
import { convertOnlineDriveData, isBucketListInitiation, isFile } from './utils'
// ==========================================
// Mock Modules
@ -198,7 +198,7 @@ const createMockOnlineDriveFile = (overrides?: Partial<OnlineDriveFile>): Online
...overrides,
})
const createMockCredential = (overrides?: Partial<{ id: string; name: string }>) => ({
const createMockCredential = (overrides?: Partial<{ id: string, name: string }>) => ({
id: 'cred-1',
name: 'Test Credential',
avatar_url: 'https://example.com/avatar.png',

View File

@ -1,21 +1,21 @@
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import Header from '../base/header'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import FileList from './file-list'
import type { OnlineDriveFile } from '@/models/pipeline'
import { DatasourceType, OnlineDriveFileType } from '@/models/pipeline'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { ssePost } from '@/service/base'
import type { DataSourceNodeCompletedResponse, DataSourceNodeErrorResponse } from '@/types/pipeline'
import Toast from '@/app/components/base/toast'
import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store'
import { convertOnlineDriveData } from './utils'
import { produce } from 'immer'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useShallow } from 'zustand/react/shallow'
import { useModalContextSelector } from '@/context/modal-context'
import { useGetDataSourceAuth } from '@/service/use-datasource'
import { useDocLink } from '@/context/i18n'
import Toast from '@/app/components/base/toast'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useDocLink } from '@/context/i18n'
import { useModalContextSelector } from '@/context/modal-context'
import { DatasourceType, OnlineDriveFileType } from '@/models/pipeline'
import { ssePost } from '@/service/base'
import { useGetDataSourceAuth } from '@/service/use-datasource'
import Header from '../base/header'
import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store'
import FileList from './file-list'
import { convertOnlineDriveData } from './utils'
type OnlineDriveProps = {
nodeId: string
@ -69,7 +69,8 @@ const OnlineDrive = ({
: `/rag/pipelines/${pipelineId}/workflows/draft/datasource/nodes/${nodeId}/run`
const getOnlineDriveFiles = useCallback(async () => {
if (isLoadingRef.current) return
if (isLoadingRef.current)
return
const { nextPageParameters, prefix, bucket, onlineDriveFileList, currentCredentialId } = dataSourceStore.getState()
setIsLoading(true)
isLoadingRef.current = true
@ -116,7 +117,8 @@ const OnlineDrive = ({
}, [dataSourceStore, datasourceNodeRunURL, breadcrumbs])
useEffect(() => {
if (!currentCredentialId) return
if (!currentCredentialId)
return
if (isInitialMount) {
// Only fetch files on initial mount if fileList is empty
if (onlineDriveFileList.length === 0)
@ -147,14 +149,16 @@ const OnlineDrive = ({
const handleSelectFile = useCallback((file: OnlineDriveFile) => {
const { selectedFileIds, setSelectedFileIds } = dataSourceStore.getState()
if (file.type === OnlineDriveFileType.bucket) return
if (file.type === OnlineDriveFileType.bucket)
return
const newSelectedFileList = produce(selectedFileIds, (draft) => {
if (draft.includes(file.id)) {
const index = draft.indexOf(file.id)
draft.splice(index, 1)
}
else {
if (!supportBatchUpload && draft.length >= 1) return
if (!supportBatchUpload && draft.length >= 1)
return
draft.push(file.id)
}
})
@ -163,7 +167,8 @@ const OnlineDrive = ({
const handleOpenFolder = useCallback((file: OnlineDriveFile) => {
const { breadcrumbs, prefix, setBreadcrumbs, setPrefix, setBucket, setOnlineDriveFileList, setSelectedFileIds } = dataSourceStore.getState()
if (file.type === OnlineDriveFileType.file) return
if (file.type === OnlineDriveFileType.file)
return
setOnlineDriveFileList([])
if (file.type === OnlineDriveFileType.bucket) {
setBucket(file.name)
@ -188,9 +193,9 @@ const OnlineDrive = ({
}, [setShowAccountSettingModal])
return (
<div className='flex flex-col gap-y-2'>
<div className="flex flex-col gap-y-2">
<Header
docTitle='Docs'
docTitle="Docs"
docLink={docLink('/guides/knowledge-base/knowledge-pipeline/authorize-data-source')}
onClickConfiguration={handleSetting}
pluginName={nodeData.datasource_label}

View File

@ -1,19 +1,21 @@
import { type OnlineDriveFile, OnlineDriveFileType } from '@/models/pipeline'
import type { OnlineDriveFile } from '@/models/pipeline'
import type { OnlineDriveData } from '@/types/pipeline'
import { OnlineDriveFileType } from '@/models/pipeline'
export const isFile = (type: 'file' | 'folder'): boolean => {
return type === 'file'
}
export const isBucketListInitiation = (data: OnlineDriveData[], prefix: string[], bucket: string): boolean => {
if (bucket || prefix.length > 0) return false
if (bucket || prefix.length > 0)
return false
const hasBucket = data.every(item => !!item.bucket)
return hasBucket && (data.length > 1 || (data.length === 1 && !!data[0].bucket && data[0].files.length === 0))
}
export const convertOnlineDriveData = (data: OnlineDriveData[], prefix: string[], bucket: string): {
fileList: OnlineDriveFile[],
isTruncated: boolean,
fileList: OnlineDriveFile[]
isTruncated: boolean
nextPageParameters: Record<string, any>
hasBucket: boolean
} => {

View File

@ -1,16 +1,16 @@
import type { CommonShape } from './slices/common'
import type { LocalFileSliceShape } from './slices/local-file'
import type { OnlineDocumentSliceShape } from './slices/online-document'
import type { OnlineDriveSliceShape } from './slices/online-drive'
import type { WebsiteCrawlSliceShape } from './slices/website-crawl'
import { useContext } from 'react'
import { createStore, useStore } from 'zustand'
import { DataSourceContext } from './provider'
import type { CommonShape } from './slices/common'
import { createCommonSlice } from './slices/common'
import type { LocalFileSliceShape } from './slices/local-file'
import { createLocalFileSlice } from './slices/local-file'
import type { OnlineDocumentSliceShape } from './slices/online-document'
import { createOnlineDocumentSlice } from './slices/online-document'
import type { WebsiteCrawlSliceShape } from './slices/website-crawl'
import { createWebsiteCrawlSlice } from './slices/website-crawl'
import type { OnlineDriveSliceShape } from './slices/online-drive'
import { createOnlineDriveSlice } from './slices/online-drive'
import { createWebsiteCrawlSlice } from './slices/website-crawl'
export type DataSourceShape = CommonShape
& LocalFileSliceShape

View File

@ -1,8 +1,8 @@
'use client'
import React from 'react'
import { cn } from '@/utils/classnames'
import Checkbox from '@/app/components/base/checkbox'
import Tooltip from '@/app/components/base/tooltip'
import { cn } from '@/utils/classnames'
type CheckboxWithLabelProps = {
className?: string
@ -28,9 +28,9 @@ const CheckboxWithLabel = ({
{tooltip && (
<Tooltip
popupContent={
<div className='w-[200px]'>{tooltip}</div>
<div className="w-[200px]">{tooltip}</div>
}
triggerClassName='ml-0.5 w-4 h-4'
triggerClassName="ml-0.5 w-4 h-4"
/>
)}
</label>

View File

@ -1,11 +1,11 @@
'use client'
import React, { useCallback } from 'react'
import { cn } from '@/utils/classnames'
import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets'
import Checkbox from '@/app/components/base/checkbox'
import Button from '@/app/components/base/button'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Checkbox from '@/app/components/base/checkbox'
import Radio from '@/app/components/base/radio/ui'
import { cn } from '@/utils/classnames'
type CrawledResultItemProps = {
payload: CrawlResultItemType
@ -36,31 +36,34 @@ const CrawledResultItem = ({
<div className={cn(
'relative flex cursor-pointer gap-x-2 rounded-lg p-2',
isPreview ? 'bg-state-base-active' : 'group hover:bg-state-base-hover',
)}>
)}
>
{
isMultipleChoice ? (
<Checkbox
className='shrink-0'
checked={isChecked}
onCheck={handleCheckChange}
/>
) : (
<Radio
className='shrink-0'
isChecked={isChecked}
onCheck={handleCheckChange}
/>
)
isMultipleChoice
? (
<Checkbox
className="shrink-0"
checked={isChecked}
onCheck={handleCheckChange}
/>
)
: (
<Radio
className="shrink-0"
isChecked={isChecked}
onCheck={handleCheckChange}
/>
)
}
<div className='flex min-w-0 grow flex-col gap-y-0.5'>
<div className="flex min-w-0 grow flex-col gap-y-0.5">
<div
className='system-sm-medium truncate text-text-secondary'
className="system-sm-medium truncate text-text-secondary"
title={payload.title}
>
{payload.title}
</div>
<div
className='system-xs-regular truncate text-text-tertiary'
className="system-xs-regular truncate text-text-tertiary"
title={payload.source_url}
>
{payload.source_url}
@ -68,9 +71,9 @@ const CrawledResultItem = ({
</div>
{showPreview && (
<Button
size='small'
size="small"
onClick={onPreview}
className='system-xs-medium-uppercase right-2 top-2 hidden px-1.5 group-hover:absolute group-hover:block'
className="system-xs-medium-uppercase right-2 top-2 hidden px-1.5 group-hover:absolute group-hover:block"
>
{t('datasetCreation.stepOne.website.preview')}
</Button>

View File

@ -1,8 +1,8 @@
'use client'
import type { CrawlResultItem } from '@/models/datasets'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
import type { CrawlResultItem } from '@/models/datasets'
import CheckboxWithLabel from './checkbox-with-label'
import CrawledResultItem from './crawled-result-item'
@ -56,28 +56,30 @@ const CrawledResult = ({
}, [checkedList, onSelectedChange, isMultipleChoice])
const handlePreview = useCallback((index: number) => {
if (!onPreview) return
if (!onPreview)
return
onPreview(list[index], index)
}, [list, onPreview])
return (
<div className={cn('flex flex-col gap-y-2', className)}>
<div className='system-sm-medium pt-2 text-text-primary'>
<div className="system-sm-medium pt-2 text-text-primary">
{t(`${I18N_PREFIX}.scrapTimeInfo`, {
total: list.length,
time: usedTime.toFixed(1),
})}
</div>
<div className='overflow-hidden rounded-xl border border-components-panel-border bg-components-panel-bg'>
<div className="overflow-hidden rounded-xl border border-components-panel-border bg-components-panel-bg">
{isMultipleChoice && (
<div className='flex items-center px-4 py-2'>
<div className="flex items-center px-4 py-2">
<CheckboxWithLabel
isChecked={isCheckAll}
onChange={handleCheckedAll} label={isCheckAll ? t(`${I18N_PREFIX}.resetAll`) : t(`${I18N_PREFIX}.selectAll`)}
onChange={handleCheckedAll}
label={isCheckAll ? t(`${I18N_PREFIX}.resetAll`) : t(`${I18N_PREFIX}.selectAll`)}
/>
</div>
)}
<div className='flex flex-col gap-y-px border-t border-divider-subtle bg-background-default-subtle p-2'>
<div className="flex flex-col gap-y-px border-t border-divider-subtle bg-background-default-subtle p-2">
{list.map((item, index) => (
<CrawledResultItem
key={item.source_url}

View File

@ -29,15 +29,15 @@ const Item = React.memo(({
secondLineWidth,
}: ItemProps) => {
return (
<div className='flex gap-x-2 px-2 py-[5px]'>
<div className='py-0.5'>
<Block className='size-4 rounded-[4px]' />
<div className="flex gap-x-2 px-2 py-[5px]">
<div className="py-0.5">
<Block className="size-4 rounded-[4px]" />
</div>
<div className='flex grow flex-col'>
<div className='flex h-5 w-full items-center'>
<div className="flex grow flex-col">
<div className="flex h-5 w-full items-center">
<Block className={cn('h-2.5 rounded-sm', firstLineWidth)} />
</div>
<div className='flex h-[18px] w-full items-center'>
<div className="flex h-[18px] w-full items-center">
<Block className={cn('h-1.5 rounded-sm', secondLineWidth)} />
</div>
</div>
@ -65,15 +65,19 @@ const Crawling = ({
return (
<div className={cn('mt-2 flex flex-col gap-y-2 pt-2', className)}>
<div className='system-sm-medium text-text-primary'>
{t('datasetCreation.stepOne.website.totalPageScraped')} {crawledNum}/{totalNum}
<div className="system-sm-medium text-text-primary">
{t('datasetCreation.stepOne.website.totalPageScraped')}
{' '}
{crawledNum}
/
{totalNum}
</div>
<div className='overflow-hidden rounded-xl border border-components-panel-border bg-components-panel-bg'>
<div className='flex items-center gap-x-2 px-4 py-2'>
<Block className='size-4 rounded-[4px]' />
<Block className='h-2.5 w-14 rounded-sm' />
<div className="overflow-hidden rounded-xl border border-components-panel-border bg-components-panel-bg">
<div className="flex items-center gap-x-2 px-4 py-2">
<Block className="size-4 rounded-[4px]" />
<Block className="h-2.5 w-14 rounded-sm" />
</div>
<div className='flex flex-col gap-px border-t border-divider-subtle bg-background-default-subtle p-2'>
<div className="flex flex-col gap-px border-t border-divider-subtle bg-background-default-subtle p-2">
{itemsConfig.map((item, index) => (
<Item
key={index}

View File

@ -1,6 +1,6 @@
import { RiErrorWarningFill } from '@remixicon/react'
import React from 'react'
import { cn } from '@/utils/classnames'
import { RiErrorWarningFill } from '@remixicon/react'
type ErrorMessageProps = {
className?: string
@ -18,14 +18,15 @@ const ErrorMessage = ({
<div className={cn(
'flex gap-x-0.5 rounded-xl border-[0.5px] border-components-panel-border bg-opacity-40 bg-toast-error-bg p-2 shadow-xs shadow-shadow-shadow-3',
className,
)}>
<div className='flex size-6 items-center justify-center'>
<RiErrorWarningFill className='h-4 w-4 text-text-destructive' />
)}
>
<div className="flex size-6 items-center justify-center">
<RiErrorWarningFill className="h-4 w-4 text-text-destructive" />
</div>
<div className='flex flex-col gap-y-0.5 py-1'>
<div className='system-xs-medium text-text-primary'>{title}</div>
<div className="flex flex-col gap-y-0.5 py-1">
<div className="system-xs-medium text-text-primary">{title}</div>
{errorMsg && (
<div className='system-xs-regular text-text-secondary'>{errorMsg}</div>
<div className="system-xs-regular text-text-secondary">{errorMsg}</div>
)}
</div>
</div>

View File

@ -1,11 +1,11 @@
import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import CheckboxWithLabel from './checkbox-with-label'
import CrawledResultItem from './crawled-result-item'
import CrawledResult from './crawled-result'
import CrawledResultItem from './crawled-result-item'
import Crawling from './crawling'
import ErrorMessage from './error-message'
import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets'
// ==========================================
// Test Data Builders
@ -24,8 +24,7 @@ const createMockCrawlResultItems = (count = 3): CrawlResultItemType[] => {
createMockCrawlResultItem({
source_url: `https://example.com/page${i + 1}`,
title: `Page ${i + 1}`,
}),
)
}))
}
// ==========================================

View File

@ -1,12 +1,12 @@
import type { MockInstance } from 'vitest'
import type { RAGPipelineVariables } from '@/models/pipeline'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import Options from './index'
import { CrawlStep } from '@/models/datasets'
import type { RAGPipelineVariables } from '@/models/pipeline'
import { PipelineInputVarType } from '@/models/pipeline'
import Toast from '@/app/components/base/toast'
import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/types'
import Toast from '@/app/components/base/toast'
import { CrawlStep } from '@/models/datasets'
import { PipelineInputVarType } from '@/models/pipeline'
import Options from './index'
// ==========================================
// Mock Modules
@ -88,8 +88,7 @@ const createMockVariables = (count = 1): RAGPipelineVariables => {
createMockVariable({
variable: `variable_${i}`,
label: `Label ${i}`,
}),
)
}))
}
const createMockConfiguration = (overrides?: Partial<any>): any => ({
@ -796,8 +795,7 @@ describe('Options', () => {
it('should handle many configurations', () => {
// Arrange
const manyConfigs = Array.from({ length: 10 }, (_, i) =>
createMockConfiguration({ variable: `field_${i}`, label: `Field ${i}` }),
)
createMockConfiguration({ variable: `field_${i}`, label: `Field ${i}` }))
mockUseConfigurations.mockReturnValue(manyConfigs)
const props = createDefaultProps()

View File

@ -1,17 +1,17 @@
import Button from '@/app/components/base/button'
import { useAppForm } from '@/app/components/base/form'
import BaseField from '@/app/components/base/form/form-scenarios/base/field'
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
import { cn } from '@/utils/classnames'
import type { RAGPipelineVariables } from '@/models/pipeline'
import { RiPlayLargeLine } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import { useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast'
import type { RAGPipelineVariables } from '@/models/pipeline'
import { useConfigurations, useInitialData } from '@/app/components/rag-pipeline/hooks/use-input-fields'
import Button from '@/app/components/base/button'
import { useAppForm } from '@/app/components/base/form'
import BaseField from '@/app/components/base/form/form-scenarios/base/field'
import { generateZodSchema } from '@/app/components/base/form/form-scenarios/base/utils'
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
import Toast from '@/app/components/base/toast'
import { useConfigurations, useInitialData } from '@/app/components/rag-pipeline/hooks/use-input-fields'
import { CrawlStep } from '@/models/datasets'
import { cn } from '@/utils/classnames'
const I18N_PREFIX = 'datasetCreation.stepOne.website'
@ -76,37 +76,37 @@ const Options = ({
return (
<form
className='w-full'
className="w-full"
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
form.handleSubmit()
}}
>
<div className='flex items-center gap-x-1 px-4 py-2'>
<div className="flex items-center gap-x-1 px-4 py-2">
<div
className='flex grow cursor-pointer select-none items-center gap-x-0.5'
className="flex grow cursor-pointer select-none items-center gap-x-0.5"
onClick={foldToggle}
>
<span className='system-sm-semibold-uppercase text-text-secondary'>
<span className="system-sm-semibold-uppercase text-text-secondary">
{t(`${I18N_PREFIX}.options`)}
</span>
<ArrowDownRoundFill className={cn('h-4 w-4 shrink-0 text-text-quaternary', fold && '-rotate-90')} />
</div>
<Button
variant='primary'
variant="primary"
onClick={form.handleSubmit}
disabled={runDisabled || isRunning}
loading={isRunning}
className='shrink-0 gap-x-0.5'
spinnerClassName='!ml-0'
className="shrink-0 gap-x-0.5"
spinnerClassName="!ml-0"
>
<RiPlayLargeLine className='size-4' />
<span className='px-0.5'>{!isRunning ? t(`${I18N_PREFIX}.run`) : t(`${I18N_PREFIX}.running`)}</span>
<RiPlayLargeLine className="size-4" />
<span className="px-0.5">{!isRunning ? t(`${I18N_PREFIX}.run`) : t(`${I18N_PREFIX}.running`)}</span>
</Button>
</div>
{!fold && (
<div className='flex flex-col gap-3 border-t border-divider-subtle px-4 py-3'>
<div className="flex flex-col gap-3 border-t border-divider-subtle px-4 py-3">
{configurations.map((config, index) => {
const FieldComponent = BaseField({
initialData,

View File

@ -1,10 +1,10 @@
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import type { CrawlResultItem } from '@/models/datasets'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import React from 'react'
import WebsiteCrawl from './index'
import type { CrawlResultItem } from '@/models/datasets'
import { CrawlStep } from '@/models/datasets'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { CrawlStep } from '@/models/datasets'
import WebsiteCrawl from './index'
// ==========================================
// Mock Modules
@ -63,7 +63,7 @@ vi.mock('@/service/use-pipeline', () => ({
// Mock store
const mockStoreState = {
crawlResult: undefined as { data: CrawlResultItem[]; time_consuming: number | string } | undefined,
crawlResult: undefined as { data: CrawlResultItem[], time_consuming: number | string } | undefined,
step: CrawlStep.init,
websitePages: [] as CrawlResultItem[],
previewIndex: -1,
@ -188,7 +188,7 @@ const createMockCrawlResultItem = (overrides?: Partial<CrawlResultItem>): CrawlR
...overrides,
})
const createMockCredential = (overrides?: Partial<{ id: string; name: string }>) => ({
const createMockCredential = (overrides?: Partial<{ id: string, name: string }>) => ({
id: 'cred-1',
name: 'Test Credential',
avatar_url: 'https://example.com/avatar.png',

View File

@ -1,32 +1,32 @@
'use client'
import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import type { CrawlResultItem } from '@/models/datasets'
import { CrawlStep } from '@/models/datasets'
import Header from '../base/header'
import Options from './base/options'
import Crawling from './base/crawling'
import ErrorMessage from './base/error-message'
import CrawledResult from './base/crawled-result'
import {
useDraftPipelinePreProcessingParams,
usePublishedPipelinePreProcessingParams,
} from '@/service/use-pipeline'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { DatasourceType } from '@/models/pipeline'
import { ssePost } from '@/service/base'
import type {
DataSourceNodeCompletedResponse,
DataSourceNodeErrorResponse,
DataSourceNodeProcessingResponse,
} from '@/types/pipeline'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store'
import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
import { useModalContextSelector } from '@/context/modal-context'
import { useGetDataSourceAuth } from '@/service/use-datasource'
import { useDocLink } from '@/context/i18n'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useDocLink } from '@/context/i18n'
import { useModalContextSelector } from '@/context/modal-context'
import { CrawlStep } from '@/models/datasets'
import { DatasourceType } from '@/models/pipeline'
import { ssePost } from '@/service/base'
import { useGetDataSourceAuth } from '@/service/use-datasource'
import {
useDraftPipelinePreProcessingParams,
usePublishedPipelinePreProcessingParams,
} from '@/service/use-pipeline'
import Header from '../base/header'
import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store'
import CrawledResult from './base/crawled-result'
import Crawling from './base/crawling'
import ErrorMessage from './base/error-message'
import Options from './base/options'
const I18N_PREFIX = 'datasetCreation.stepOne.website'
@ -154,9 +154,9 @@ const WebsiteCrawl = ({
}, [onCredentialChange])
return (
<div className='flex flex-col'>
<div className="flex flex-col">
<Header
docTitle='Docs'
docTitle="Docs"
docLink={docLink('/guides/knowledge-base/knowledge-pipeline/authorize-data-source')}
onClickConfiguration={handleSetting}
pluginName={nodeData.datasource_label}
@ -164,7 +164,7 @@ const WebsiteCrawl = ({
onCredentialChange={handleCredentialChange}
credentials={dataSourceAuth?.result || []}
/>
<div className='mt-2 rounded-xl border border-components-panel-border bg-background-default-subtle'>
<div className="mt-2 rounded-xl border border-components-panel-border bg-background-default-subtle">
<Options
variables={paramsConfig?.variables || []}
step={step}
@ -173,7 +173,7 @@ const WebsiteCrawl = ({
/>
</div>
{!isInit && (
<div className='relative flex flex-col'>
<div className="relative flex flex-col">
{isRunning && (
<Crawling
crawledNum={crawledNum}
@ -182,14 +182,14 @@ const WebsiteCrawl = ({
)}
{showError && (
<ErrorMessage
className='mt-2'
className="mt-2"
title={t(`${I18N_PREFIX}.exceptionErrorTitle`)}
errorMsg={crawlErrorMessage}
/>
)}
{isCrawlFinished && !showError && (
<CrawledResult
className='mt-2'
className="mt-2"
list={crawlResult?.data || []}
checkedList={checkedCrawlResult}
onSelectedChange={handleCheckedCrawlResultChange}

View File

@ -1,13 +1,14 @@
import { useTranslation } from 'react-i18next'
import { AddDocumentsStep } from './types'
import type { DataSourceOption } from '@/app/components/rag-pipeline/components/panel/test-run/types'
import { useCallback, useMemo, useState } from 'react'
import { BlockEnum, type Node } from '@/app/components/workflow/types'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import { useDataSourceStore, useDataSourceStoreWithSelector } from './data-source/store'
import type { Node } from '@/app/components/workflow/types'
import type { DataSourceNotionPageMap, DataSourceNotionWorkspace } from '@/models/common'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow'
import { BlockEnum } from '@/app/components/workflow/types'
import { CrawlStep } from '@/models/datasets'
import { useDataSourceStore, useDataSourceStoreWithSelector } from './data-source/store'
import { AddDocumentsStep } from './types'
export const useAddDocumentsSteps = () => {
const { t } = useTranslation()

View File

@ -1,46 +1,46 @@
'use client'
import { useCallback, useMemo, useRef, useState } from 'react'
import DataSourceOptions from './data-source-options'
import type { CrawlResultItem, DocumentItem, CustomFile as File, FileIndexingEstimateResponse } from '@/models/datasets'
import LocalFile from '@/app/components/datasets/documents/create-from-pipeline/data-source/local-file'
import { useProviderContextSelector } from '@/context/provider-context'
import type { NotionPage } from '@/models/common'
import OnlineDocuments from '@/app/components/datasets/documents/create-from-pipeline/data-source/online-documents'
import VectorSpaceFull from '@/app/components/billing/vector-space-full'
import WebsiteCrawl from '@/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl'
import OnlineDrive from '@/app/components/datasets/documents/create-from-pipeline/data-source/online-drive'
import Actions from './actions'
import { useTranslation } from 'react-i18next'
import type { Datasource } from '@/app/components/rag-pipeline/components/panel/test-run/types'
import LeftHeader from './left-header'
import { usePublishedPipelineInfo, useRunPublishedPipeline } from '@/service/use-pipeline'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import Loading from '@/app/components/base/loading'
import type { Node } from '@/app/components/workflow/types'
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import FilePreview from './preview/file-preview'
import OnlineDocumentPreview from './preview/online-document-preview'
import WebsitePreview from './preview/web-preview'
import ProcessDocuments from './process-documents'
import ChunkPreview from './preview/chunk-preview'
import Processing from './processing'
import type { Node } from '@/app/components/workflow/types'
import type { NotionPage } from '@/models/common'
import type { CrawlResultItem, DocumentItem, CustomFile as File, FileIndexingEstimateResponse } from '@/models/datasets'
import type {
InitialDocumentDetail,
OnlineDriveFile,
PublishedPipelineRunPreviewResponse,
PublishedPipelineRunResponse,
} from '@/models/pipeline'
import { DatasourceType } from '@/models/pipeline'
import { TransferMethod } from '@/types/app'
import { useAddDocumentsSteps, useLocalFile, useOnlineDocument, useOnlineDrive, useWebsiteCrawl } from './hooks'
import DataSourceProvider from './data-source/store/provider'
import { useDataSourceStore } from './data-source/store'
import { useFileUploadConfig } from '@/service/use-common'
import UpgradeCard from '../../create/step-one/upgrade-card'
import Divider from '@/app/components/base/divider'
import { useBoolean } from 'ahooks'
import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
import { useCallback, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { trackEvent } from '@/app/components/base/amplitude'
import Divider from '@/app/components/base/divider'
import Loading from '@/app/components/base/loading'
import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
import VectorSpaceFull from '@/app/components/billing/vector-space-full'
import LocalFile from '@/app/components/datasets/documents/create-from-pipeline/data-source/local-file'
import OnlineDocuments from '@/app/components/datasets/documents/create-from-pipeline/data-source/online-documents'
import OnlineDrive from '@/app/components/datasets/documents/create-from-pipeline/data-source/online-drive'
import WebsiteCrawl from '@/app/components/datasets/documents/create-from-pipeline/data-source/website-crawl'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useProviderContextSelector } from '@/context/provider-context'
import { DatasourceType } from '@/models/pipeline'
import { useFileUploadConfig } from '@/service/use-common'
import { usePublishedPipelineInfo, useRunPublishedPipeline } from '@/service/use-pipeline'
import { TransferMethod } from '@/types/app'
import UpgradeCard from '../../create/step-one/upgrade-card'
import Actions from './actions'
import DataSourceOptions from './data-source-options'
import { useDataSourceStore } from './data-source/store'
import DataSourceProvider from './data-source/store/provider'
import { useAddDocumentsSteps, useLocalFile, useOnlineDocument, useOnlineDrive, useWebsiteCrawl } from './hooks'
import LeftHeader from './left-header'
import ChunkPreview from './preview/chunk-preview'
import FilePreview from './preview/file-preview'
import OnlineDocumentPreview from './preview/online-document-preview'
import WebsitePreview from './preview/web-preview'
import ProcessDocuments from './process-documents'
import Processing from './processing'
const CreateFormPipeline = () => {
const { t } = useTranslation()
@ -137,7 +137,8 @@ const CreateFormPipeline = () => {
}, [datasourceType, doHandleNextStep, localFileList.length, onlineDocuments.length, selectedFileIds.length, showPlanUpgradeModal, supportBatchUpload, websitePages.length])
const nextBtnDisabled = useMemo(() => {
if (!datasource) return true
if (!datasource)
return true
if (datasourceType === DatasourceType.localFile)
return isShowVectorSpaceFull || !localFileList.length || !allFileLoaded
if (datasourceType === DatasourceType.onlineDocument)
@ -454,25 +455,25 @@ const CreateFormPipeline = () => {
if (isFetchingPipelineInfo) {
return (
<Loading type='app' />
<Loading type="app" />
)
}
return (
<div
className='relative flex h-[calc(100vh-56px)] w-full min-w-[1024px] overflow-x-auto rounded-t-2xl border-t border-effects-highlight bg-background-default-subtle'
className="relative flex h-[calc(100vh-56px)] w-full min-w-[1024px] overflow-x-auto rounded-t-2xl border-t border-effects-highlight bg-background-default-subtle"
>
<div className='h-full min-w-0 flex-1'>
<div className='flex h-full flex-col px-14'>
<div className="h-full min-w-0 flex-1">
<div className="flex h-full flex-col px-14">
<LeftHeader
steps={steps}
title={t('datasetPipeline.addDocuments.title')}
currentStep={currentStep}
/>
<div className='grow overflow-y-auto'>
<div className="grow overflow-y-auto">
{
currentStep === 1 && (
<div className='flex flex-col gap-y-5 pt-4'>
<div className="flex flex-col gap-y-5 pt-4">
<DataSourceOptions
datasourceNodeId={datasource?.nodeId || ''}
onSelect={handleSwitchDataSource}
@ -520,7 +521,7 @@ const CreateFormPipeline = () => {
{
!supportBatchUpload && datasourceType === DatasourceType.localFile && localFileList.length > 0 && (
<>
<Divider type='horizontal' className='my-4 h-px bg-divider-subtle' />
<Divider type="horizontal" className="my-4 h-px bg-divider-subtle" />
<UpgradeCard />
</>
)
@ -555,8 +556,8 @@ const CreateFormPipeline = () => {
{/* Preview */}
{
currentStep === 1 && (
<div className='h-full min-w-0 flex-1'>
<div className='flex h-full flex-col pl-2 pt-2'>
<div className="h-full min-w-0 flex-1">
<div className="flex h-full flex-col pl-2 pt-2">
{currentLocalFile && (
<FilePreview
file={currentLocalFile}
@ -582,8 +583,8 @@ const CreateFormPipeline = () => {
}
{
currentStep === 2 && (
<div className='h-full min-w-0 flex-1'>
<div className='flex h-full flex-col pl-2 pt-2'>
<div className="h-full min-w-0 flex-1">
<div className="flex h-full flex-col pl-2 pt-2">
<ChunkPreview
dataSourceType={datasourceType as DatasourceType}
localFiles={localFileList.map(file => file.file)}

View File

@ -1,11 +1,11 @@
import React from 'react'
import { RiArrowLeftLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import { useParams } from 'next/navigation'
import Effect from '@/app/components/base/effect'
import type { Step } from './step-indicator'
import StepIndicator from './step-indicator'
import { RiArrowLeftLine } from '@remixicon/react'
import Link from 'next/link'
import { useParams } from 'next/navigation'
import React from 'react'
import Button from '@/app/components/base/button'
import Effect from '@/app/components/base/effect'
import StepIndicator from './step-indicator'
type LeftHeaderProps = {
steps: Array<Step>
@ -21,15 +21,15 @@ const LeftHeader = ({
const { datasetId } = useParams()
return (
<div className='relative flex flex-col gap-y-0.5 pb-2 pt-4'>
<div className='flex items-center gap-x-2'>
<span className='system-2xs-semibold-uppercase bg-pipeline-add-documents-title-bg bg-clip-text text-transparent'>
<div className="relative flex flex-col gap-y-0.5 pb-2 pt-4">
<div className="flex items-center gap-x-2">
<span className="system-2xs-semibold-uppercase bg-pipeline-add-documents-title-bg bg-clip-text text-transparent">
{title}
</span>
<span className='system-2xs-regular text-divider-regular'>/</span>
<span className="system-2xs-regular text-divider-regular">/</span>
<StepIndicator steps={steps} currentStep={currentStep} />
</div>
<div className='system-md-semibold text-text-primary'>
<div className="system-md-semibold text-text-primary">
{steps[currentStep - 1]?.label}
</div>
{currentStep !== steps.length && (
@ -38,14 +38,14 @@ const LeftHeader = ({
replace
>
<Button
variant='secondary-accent'
className='absolute -left-11 top-3.5 size-9 rounded-full p-0'
variant="secondary-accent"
className="absolute -left-11 top-3.5 size-9 rounded-full p-0"
>
<RiArrowLeftLine className='size-5 ' />
<RiArrowLeftLine className="size-5 " />
</Button>
</Link>
)}
<Effect className='left-8 top-[-34px] opacity-20' />
<Effect className="left-8 top-[-34px] opacity-20" />
</div>
)
}

View File

@ -1,11 +1,11 @@
import type { NotionPage } from '@/models/common'
import type { CrawlResultItem, CustomFile, FileIndexingEstimateResponse } from '@/models/datasets'
import type { OnlineDriveFile } from '@/models/pipeline'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import ChunkPreview from './chunk-preview'
import { ChunkingMode } from '@/models/datasets'
import type { CrawlResultItem, CustomFile, FileIndexingEstimateResponse } from '@/models/datasets'
import type { NotionPage } from '@/models/common'
import type { OnlineDriveFile } from '@/models/pipeline'
import { DatasourceType, OnlineDriveFileType } from '@/models/pipeline'
import ChunkPreview from './chunk-preview'
// Uses global react-i18next mock from web/vitest.setup.ts
@ -21,9 +21,9 @@ vi.mock('@/context/dataset-detail', () => ({
vi.mock('../../../common/document-picker/preview-document-picker', () => ({
__esModule: true,
default: ({ files, onChange, value }: {
files: Array<{ id: string; name: string; extension: string }>
onChange: (selected: { id: string; name: string; extension: string }) => void
value: { id: string; name: string; extension: string }
files: Array<{ id: string, name: string, extension: string }>
onChange: (selected: { id: string, name: string, extension: string }) => void
value: { id: string, name: string, extension: string }
}) => (
<div data-testid="document-picker">
<span data-testid="picker-value">{value?.name || 'No selection'}</span>

View File

@ -1,21 +1,21 @@
import type { NotionPage } from '@/models/common'
import type { CrawlResultItem, CustomFile, DocumentItem, FileIndexingEstimateResponse } from '@/models/datasets'
import type { OnlineDriveFile } from '@/models/pipeline'
import { RiSearchEyeLine } from '@remixicon/react'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import PreviewContainer from '../../../preview/container'
import { PreviewHeader } from '../../../preview/header'
import type { CrawlResultItem, CustomFile, DocumentItem, FileIndexingEstimateResponse } from '@/models/datasets'
import { ChunkingMode } from '@/models/datasets'
import type { NotionPage } from '@/models/common'
import PreviewDocumentPicker from '../../../common/document-picker/preview-document-picker'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { ChunkContainer, QAPreview } from '../../../chunk'
import { FormattedText } from '../../../formatted-text/formatted'
import { PreviewSlice } from '../../../formatted-text/flavours/preview-slice'
import { SkeletonContainer, SkeletonPoint, SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
import { RiSearchEyeLine } from '@remixicon/react'
import Badge from '@/app/components/base/badge'
import Button from '@/app/components/base/button'
import type { OnlineDriveFile } from '@/models/pipeline'
import { SkeletonContainer, SkeletonPoint, SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { ChunkingMode } from '@/models/datasets'
import { DatasourceType } from '@/models/pipeline'
import { ChunkContainer, QAPreview } from '../../../chunk'
import PreviewDocumentPicker from '../../../common/document-picker/preview-document-picker'
import { PreviewSlice } from '../../../formatted-text/flavours/preview-slice'
import { FormattedText } from '../../../formatted-text/formatted'
import PreviewContainer from '../../../preview/container'
import { PreviewHeader } from '../../../preview/header'
import { getFileExtension } from '../data-source/online-drive/file-list/list/utils'
type ChunkPreviewProps = {
@ -59,98 +59,106 @@ const ChunkPreview = ({
return (
<PreviewContainer
header={<PreviewHeader
title={t('datasetCreation.stepTwo.preview')}
>
<div className='flex items-center gap-1'>
{dataSourceType === DatasourceType.localFile
&& <PreviewDocumentPicker
files={localFiles as Array<Required<CustomFile>>}
onChange={(selected) => {
setPreviewFile(selected)
handlePreviewFileChange(selected)
}}
value={previewFile}
/>
}
{dataSourceType === DatasourceType.onlineDocument
&& <PreviewDocumentPicker
files={
onlineDocuments.map(page => ({
id: page.page_id,
name: page.page_name,
extension: 'md',
}))
}
onChange={(selected) => {
const selectedPage = onlineDocuments.find(page => page.page_id === selected.id)
setPreviewOnlineDocument(selectedPage!)
handlePreviewOnlineDocumentChange(selectedPage!)
}}
value={{
id: previewOnlineDocument?.page_id || '',
name: previewOnlineDocument?.page_name || '',
extension: 'md',
}}
/>
}
{dataSourceType === DatasourceType.websiteCrawl
&& <PreviewDocumentPicker
files={
websitePages.map(page => ({
id: page.source_url,
name: page.title,
extension: 'md',
}))
}
onChange={(selected) => {
const selectedPage = websitePages.find(page => page.source_url === selected.id)
setPreviewWebsitePage(selectedPage!)
handlePreviewWebsitePageChange(selectedPage!)
}}
value={
{
id: previewWebsitePage?.source_url || '',
name: previewWebsitePage?.title || '',
extension: 'md',
}
}
/>
}
{dataSourceType === DatasourceType.onlineDrive
&& <PreviewDocumentPicker
files={
onlineDriveFiles.map(file => ({
id: file.id,
name: file.name,
extension: getFileExtension(previewOnlineDriveFile?.name),
}))
}
onChange={(selected) => {
const selectedFile = onlineDriveFiles.find(file => file.id === selected.id)
setPreviewOnlineDriveFile(selectedFile!)
handlePreviewOnlineDriveFileChange(selectedFile!)
}}
value={
{
id: previewOnlineDriveFile?.id || '',
name: previewOnlineDriveFile?.name || '',
extension: getFileExtension(previewOnlineDriveFile?.name),
}
}
/>
}
{
currentDocForm !== ChunkingMode.qa
&& <Badge text={t('datasetCreation.stepTwo.previewChunkCount', {
count: estimateData?.total_segments || 0,
}) as string}
/>
}
</div>
</PreviewHeader>}
className='relative flex h-full w-full shrink-0'
mainClassName='space-y-6'
header={(
<PreviewHeader
title={t('datasetCreation.stepTwo.preview')}
>
<div className="flex items-center gap-1">
{dataSourceType === DatasourceType.localFile
&& (
<PreviewDocumentPicker
files={localFiles as Array<Required<CustomFile>>}
onChange={(selected) => {
setPreviewFile(selected)
handlePreviewFileChange(selected)
}}
value={previewFile}
/>
)}
{dataSourceType === DatasourceType.onlineDocument
&& (
<PreviewDocumentPicker
files={
onlineDocuments.map(page => ({
id: page.page_id,
name: page.page_name,
extension: 'md',
}))
}
onChange={(selected) => {
const selectedPage = onlineDocuments.find(page => page.page_id === selected.id)
setPreviewOnlineDocument(selectedPage!)
handlePreviewOnlineDocumentChange(selectedPage!)
}}
value={{
id: previewOnlineDocument?.page_id || '',
name: previewOnlineDocument?.page_name || '',
extension: 'md',
}}
/>
)}
{dataSourceType === DatasourceType.websiteCrawl
&& (
<PreviewDocumentPicker
files={
websitePages.map(page => ({
id: page.source_url,
name: page.title,
extension: 'md',
}))
}
onChange={(selected) => {
const selectedPage = websitePages.find(page => page.source_url === selected.id)
setPreviewWebsitePage(selectedPage!)
handlePreviewWebsitePageChange(selectedPage!)
}}
value={
{
id: previewWebsitePage?.source_url || '',
name: previewWebsitePage?.title || '',
extension: 'md',
}
}
/>
)}
{dataSourceType === DatasourceType.onlineDrive
&& (
<PreviewDocumentPicker
files={
onlineDriveFiles.map(file => ({
id: file.id,
name: file.name,
extension: getFileExtension(previewOnlineDriveFile?.name),
}))
}
onChange={(selected) => {
const selectedFile = onlineDriveFiles.find(file => file.id === selected.id)
setPreviewOnlineDriveFile(selectedFile!)
handlePreviewOnlineDriveFileChange(selectedFile!)
}}
value={
{
id: previewOnlineDriveFile?.id || '',
name: previewOnlineDriveFile?.name || '',
extension: getFileExtension(previewOnlineDriveFile?.name),
}
}
/>
)}
{
currentDocForm !== ChunkingMode.qa
&& (
<Badge text={t('datasetCreation.stepTwo.previewChunkCount', {
count: estimateData?.total_segments || 0,
}) as string}
/>
)
}
</div>
</PreviewHeader>
)}
className="relative flex h-full w-full shrink-0"
mainClassName="space-y-6"
>
{!isPending && currentDocForm === ChunkingMode.qa && estimateData?.qa_preview && (
estimateData?.qa_preview.map((item, index) => (
@ -192,8 +200,8 @@ const ChunkPreview = ({
label={`C-${indexForLabel}`}
text={child}
tooltip={`Child-chunk-${indexForLabel} · ${child.length} Characters`}
labelInnerClassName='text-[10px] font-semibold align-bottom leading-7'
dividerClassName='leading-7'
labelInnerClassName="text-[10px] font-semibold align-bottom leading-7"
dividerClassName="leading-7"
/>
)
})}
@ -203,10 +211,10 @@ const ChunkPreview = ({
})
)}
{isIdle && (
<div className='flex h-full w-full items-center justify-center'>
<div className='flex flex-col items-center justify-center gap-3 pb-4'>
<RiSearchEyeLine className='size-10 text-text-empty-state-icon' />
<p className='text-sm text-text-tertiary'>
<div className="flex h-full w-full items-center justify-center">
<div className="flex flex-col items-center justify-center gap-3 pb-4">
<RiSearchEyeLine className="size-10 text-text-empty-state-icon" />
<p className="text-sm text-text-tertiary">
{t('datasetCreation.stepTwo.previewChunkTip')}
</p>
<Button onClick={onPreview}>
@ -216,17 +224,17 @@ const ChunkPreview = ({
</div>
)}
{isPending && (
<div className='h-full w-full space-y-6 overflow-hidden'>
<div className="h-full w-full space-y-6 overflow-hidden">
{Array.from({ length: 10 }, (_, i) => (
<SkeletonContainer key={i}>
<SkeletonRow>
<SkeletonRectangle className='w-20' />
<SkeletonRectangle className="w-20" />
<SkeletonPoint />
<SkeletonRectangle className='w-24' />
<SkeletonRectangle className="w-24" />
</SkeletonRow>
<SkeletonRectangle className='w-full' />
<SkeletonRectangle className='w-full' />
<SkeletonRectangle className='w-[422px]' />
<SkeletonRectangle className="w-full" />
<SkeletonRectangle className="w-full" />
<SkeletonRectangle className="w-[422px]" />
</SkeletonContainer>
))}
</div>

View File

@ -1,7 +1,7 @@
import type { CustomFile as File } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import FilePreview from './file-preview'
import type { CustomFile as File } from '@/models/datasets'
// Uses global react-i18next mock from web/vitest.setup.ts

View File

@ -1,12 +1,12 @@
'use client'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from './loading'
import type { CustomFile as File } from '@/models/datasets'
import { RiCloseLine } from '@remixicon/react'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useFilePreview } from '@/service/use-common'
import DocumentFileIcon from '../../../common/document-file-icon'
import { formatFileSize, formatNumberAbbreviated } from '@/utils/format'
import DocumentFileIcon from '../../../common/document-file-icon'
import Loading from './loading'
type FilePreviewProps = {
file: File
@ -28,18 +28,18 @@ const FilePreview = ({
}, [file])
return (
<div className='flex h-full w-full flex-col rounded-t-xl border-l border-t border-components-panel-border bg-background-default-lighter shadow-md shadow-shadow-shadow-5'>
<div className='flex gap-x-2 border-b border-divider-subtle pb-3 pl-6 pr-4 pt-4'>
<div className='flex grow flex-col gap-y-1'>
<div className='system-2xs-semibold-uppercase text-text-accent'>{t('datasetPipeline.addDocuments.stepOne.preview')}</div>
<div className='title-md-semi-bold text-tex-primary'>{`${fileName}.${file.extension || ''}`}</div>
<div className='system-xs-medium flex items-center gap-x-1 text-text-tertiary'>
<div className="flex h-full w-full flex-col rounded-t-xl border-l border-t border-components-panel-border bg-background-default-lighter shadow-md shadow-shadow-shadow-5">
<div className="flex gap-x-2 border-b border-divider-subtle pb-3 pl-6 pr-4 pt-4">
<div className="flex grow flex-col gap-y-1">
<div className="system-2xs-semibold-uppercase text-text-accent">{t('datasetPipeline.addDocuments.stepOne.preview')}</div>
<div className="title-md-semi-bold text-tex-primary">{`${fileName}.${file.extension || ''}`}</div>
<div className="system-xs-medium flex items-center gap-x-1 text-text-tertiary">
<DocumentFileIcon
className='size-3.5 shrink-0'
className="size-3.5 shrink-0"
name={file.name}
extension={file.extension}
/>
<span className='uppercase'>{file.extension}</span>
<span className="uppercase">{file.extension}</span>
<span>·</span>
<span>{formatFileSize(file.size)}</span>
{fileData && (
@ -51,20 +51,20 @@ const FilePreview = ({
</div>
</div>
<button
type='button'
className='flex h-8 w-8 shrink-0 items-center justify-center'
type="button"
className="flex h-8 w-8 shrink-0 items-center justify-center"
onClick={hidePreview}
>
<RiCloseLine className='size-[18px]' />
<RiCloseLine className="size-[18px]" />
</button>
</div>
{isFetching && (
<div className='grow'>
<div className="grow">
<Loading />
</div>
)}
{!isFetching && fileData && (
<div className='body-md-regular grow overflow-hidden px-6 py-5 text-text-secondary'>
<div className="body-md-regular grow overflow-hidden px-6 py-5 text-text-secondary">
{fileData.content}
</div>
)}

View File

@ -3,47 +3,47 @@ import { SkeletonContainer, SkeletonRectangle } from '@/app/components/base/skel
const Loading = () => {
return (
<div className='flex h-full w-full flex-col gap-y-3 overflow-hidden bg-gradient-to-b from-components-panel-bg-transparent to-components-panel-bg px-6 py-5'>
<SkeletonContainer className='w-full gap-0'>
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-3/5' />
<div className="flex h-full w-full flex-col gap-y-3 overflow-hidden bg-gradient-to-b from-components-panel-bg-transparent to-components-panel-bg px-6 py-5">
<SkeletonContainer className="w-full gap-0">
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-3/5" />
</SkeletonContainer>
<SkeletonContainer className='w-full gap-0'>
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-[70%]' />
<SkeletonContainer className="w-full gap-0">
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-[70%]" />
</SkeletonContainer>
<SkeletonContainer className='w-full gap-0'>
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-[56%]' />
<SkeletonContainer className="w-full gap-0">
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-[56%]" />
</SkeletonContainer>
<SkeletonContainer className='w-full gap-0'>
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-3/5' />
<SkeletonContainer className="w-full gap-0">
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-3/5" />
</SkeletonContainer>
<SkeletonContainer className='w-full gap-0'>
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-3/5' />
<SkeletonContainer className="w-full gap-0">
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-3/5" />
</SkeletonContainer>
<SkeletonContainer className='w-full gap-0'>
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-full' />
<SkeletonRectangle className='my-1.5 w-1/2' />
<SkeletonContainer className="w-full gap-0">
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-full" />
<SkeletonRectangle className="my-1.5 w-1/2" />
</SkeletonContainer>
</div>
)

View File

@ -1,9 +1,8 @@
import { render, screen, waitFor } from '@testing-library/react'
import { fireEvent } from '@testing-library/react'
import React from 'react'
import OnlineDocumentPreview from './online-document-preview'
import type { NotionPage } from '@/models/common'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import React from 'react'
import Toast from '@/app/components/base/toast'
import OnlineDocumentPreview from './online-document-preview'
// Uses global react-i18next mock from web/vitest.setup.ts

View File

@ -1,16 +1,16 @@
'use client'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { NotionPage } from '@/models/common'
import { RiCloseLine } from '@remixicon/react'
import { formatNumberAbbreviated } from '@/utils/format'
import Loading from './loading'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Notion } from '@/app/components/base/icons/src/public/common'
import { Markdown } from '@/app/components/base/markdown'
import Toast from '@/app/components/base/toast'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { usePreviewOnlineDocument } from '@/service/use-pipeline'
import Toast from '@/app/components/base/toast'
import { Markdown } from '@/app/components/base/markdown'
import { formatNumberAbbreviated } from '@/utils/format'
import { useDataSourceStore } from '../data-source/store'
import Loading from './loading'
type OnlineDocumentPreviewProps = {
currentPage: NotionPage
@ -52,33 +52,33 @@ const OnlineDocumentPreview = ({
}, [currentPage.page_id])
return (
<div className='flex h-full w-full flex-col rounded-t-xl border-l border-t border-components-panel-border bg-background-default-lighter shadow-md shadow-shadow-shadow-5'>
<div className='flex gap-x-2 border-b border-divider-subtle pb-3 pl-6 pr-4 pt-4'>
<div className='flex grow flex-col gap-y-1'>
<div className='system-2xs-semibold-uppercase text-text-accent'>{t('datasetPipeline.addDocuments.stepOne.preview')}</div>
<div className='title-md-semi-bold text-tex-primary'>{currentPage?.page_name}</div>
<div className='system-xs-medium flex items-center gap-x-1 text-text-tertiary'>
<Notion className='size-3.5' />
<div className="flex h-full w-full flex-col rounded-t-xl border-l border-t border-components-panel-border bg-background-default-lighter shadow-md shadow-shadow-shadow-5">
<div className="flex gap-x-2 border-b border-divider-subtle pb-3 pl-6 pr-4 pt-4">
<div className="flex grow flex-col gap-y-1">
<div className="system-2xs-semibold-uppercase text-text-accent">{t('datasetPipeline.addDocuments.stepOne.preview')}</div>
<div className="title-md-semi-bold text-tex-primary">{currentPage?.page_name}</div>
<div className="system-xs-medium flex items-center gap-x-1 text-text-tertiary">
<Notion className="size-3.5" />
<span>{currentPage.type}</span>
<span>·</span>
<span>{`${formatNumberAbbreviated(content.length)} ${t('datasetPipeline.addDocuments.characters')}`}</span>
</div>
</div>
<button
type='button'
className='flex h-8 w-8 shrink-0 items-center justify-center'
type="button"
className="flex h-8 w-8 shrink-0 items-center justify-center"
onClick={hidePreview}
>
<RiCloseLine className='size-[18px]' />
<RiCloseLine className="size-[18px]" />
</button>
</div>
{isPending && (
<div className='grow'>
<div className="grow">
<Loading />
</div>
)}
{!isPending && content && (
<div className='body-md-regular grow overflow-hidden px-6 py-5 text-text-secondary'>
<div className="body-md-regular grow overflow-hidden px-6 py-5 text-text-secondary">
<Markdown content={content} />
</div>
)}

View File

@ -1,7 +1,7 @@
import type { CrawlResultItem } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import WebsitePreview from './web-preview'
import type { CrawlResultItem } from '@/models/datasets'
// Uses global react-i18next mock from web/vitest.setup.ts

View File

@ -1,8 +1,8 @@
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
import type { CrawlResultItem } from '@/models/datasets'
import { RiCloseLine, RiGlobalLine } from '@remixicon/react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { formatNumberAbbreviated } from '@/utils/format'
type WebsitePreviewProps = {
@ -17,28 +17,28 @@ const WebsitePreview = ({
const { t } = useTranslation()
return (
<div className='flex h-full w-full flex-col rounded-t-xl border-l border-t border-components-panel-border bg-background-default-lighter shadow-md shadow-shadow-shadow-5'>
<div className='flex gap-x-2 border-b border-divider-subtle pb-3 pl-6 pr-4 pt-4'>
<div className='flex grow flex-col gap-y-1'>
<div className='system-2xs-semibold-uppercase'>{t('datasetPipeline.addDocuments.stepOne.preview')}</div>
<div className='title-md-semi-bold text-tex-primary'>{currentWebsite.title}</div>
<div className='system-xs-medium flex gap-x-1 text-text-tertiary'>
<RiGlobalLine className='size-3.5' />
<span className='uppercase' title={currentWebsite.source_url}>{currentWebsite.source_url}</span>
<div className="flex h-full w-full flex-col rounded-t-xl border-l border-t border-components-panel-border bg-background-default-lighter shadow-md shadow-shadow-shadow-5">
<div className="flex gap-x-2 border-b border-divider-subtle pb-3 pl-6 pr-4 pt-4">
<div className="flex grow flex-col gap-y-1">
<div className="system-2xs-semibold-uppercase">{t('datasetPipeline.addDocuments.stepOne.preview')}</div>
<div className="title-md-semi-bold text-tex-primary">{currentWebsite.title}</div>
<div className="system-xs-medium flex gap-x-1 text-text-tertiary">
<RiGlobalLine className="size-3.5" />
<span className="uppercase" title={currentWebsite.source_url}>{currentWebsite.source_url}</span>
<span>·</span>
<span>·</span>
<span>{`${formatNumberAbbreviated(currentWebsite.markdown.length)} ${t('datasetPipeline.addDocuments.characters')}`}</span>
</div>
</div>
<button
type='button'
className='flex h-8 w-8 shrink-0 items-center justify-center'
type="button"
className="flex h-8 w-8 shrink-0 items-center justify-center"
onClick={hidePreview}
>
<RiCloseLine className='size-[18px]' />
<RiCloseLine className="size-[18px]" />
</button>
</div>
<div className='body-md-regular grow overflow-hidden px-6 py-5 text-text-secondary'>
<div className="body-md-regular grow overflow-hidden px-6 py-5 text-text-secondary">
{currentWebsite.markdown}
</div>
</div>

View File

@ -1,7 +1,7 @@
import React from 'react'
import Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next'
import { RiArrowLeftLine } from '@remixicon/react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
type ActionsProps = {
onBack: () => void
@ -17,17 +17,17 @@ const Actions = ({
const { t } = useTranslation()
return (
<div className='flex items-center justify-between'>
<div className="flex items-center justify-between">
<Button
variant='secondary'
variant="secondary"
onClick={onBack}
className='gap-x-0.5'
className="gap-x-0.5"
>
<RiArrowLeftLine className='size-4' />
<span className='px-0.5'>{t('datasetPipeline.operations.dataSource')}</span>
<RiArrowLeftLine className="size-4" />
<span className="px-0.5">{t('datasetPipeline.operations.dataSource')}</span>
</Button>
<Button
variant='primary'
variant="primary"
disabled={runDisabled}
onClick={onProcess}
>

View File

@ -1,12 +1,12 @@
import type { BaseConfiguration } from '@/app/components/base/form/form-scenarios/base/types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import React from 'react'
import Actions from './actions'
import Header from './header'
import Form from './form'
import type { BaseConfiguration } from '@/app/components/base/form/form-scenarios/base/types'
import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/types'
import { z } from 'zod'
import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/types'
import Toast from '@/app/components/base/toast'
import Actions from './actions'
import Form from './form'
import Header from './header'
// ==========================================
// Spy on Toast.notify for validation tests

View File

@ -1,9 +1,9 @@
import type { ZodSchema } from 'zod'
import type { BaseConfiguration } from '@/app/components/base/form/form-scenarios/base/types'
import { useCallback, useImperativeHandle } from 'react'
import { useAppForm } from '@/app/components/base/form'
import BaseField from '@/app/components/base/form/form-scenarios/base/field'
import type { BaseConfiguration } from '@/app/components/base/form/form-scenarios/base/types'
import Toast from '@/app/components/base/toast'
import { useCallback, useImperativeHandle } from 'react'
import type { ZodSchema } from 'zod'
import Header from './header'
type OptionsProps = {
@ -62,7 +62,7 @@ const Form = ({
return (
<form
className='flex w-full flex-col rounded-lg border border-components-panel-border bg-components-panel-bg'
className="flex w-full flex-col rounded-lg border border-components-panel-border bg-components-panel-bg"
onSubmit={(e) => {
e.preventDefault()
e.stopPropagation()
@ -80,7 +80,7 @@ const Form = ({
/>
)}
/>
<div className='flex flex-col gap-3 border-t border-divider-subtle px-4 py-3'>
<div className="flex flex-col gap-3 border-t border-divider-subtle px-4 py-3">
{configurations.map((config, index) => {
const FieldComponent = BaseField({
initialData,

View File

@ -1,7 +1,7 @@
import React from 'react'
import Button from '@/app/components/base/button'
import { useTranslation } from 'react-i18next'
import { RiSearchEyeLine } from '@remixicon/react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
type HeaderProps = {
onReset: () => void
@ -19,21 +19,21 @@ const Header = ({
const { t } = useTranslation()
return (
<div className='flex items-center gap-x-1 px-4 py-2'>
<div className='system-sm-semibold-uppercase grow text-text-secondary'>
<div className="flex items-center gap-x-1 px-4 py-2">
<div className="system-sm-semibold-uppercase grow text-text-secondary">
{t('datasetPipeline.addDocuments.stepTwo.chunkSettings')}
</div>
<Button variant='ghost' disabled={resetDisabled} onClick={onReset}>
<Button variant="ghost" disabled={resetDisabled} onClick={onReset}>
{t('common.operation.reset')}
</Button>
<Button
variant='secondary-accent'
variant="secondary-accent"
onClick={onPreview}
className='gap-x-0.5'
className="gap-x-0.5"
disabled={previewDisabled}
>
<RiSearchEyeLine className='size-4' />
<span className='px-0.5'>{t('datasetPipeline.addDocuments.stepTwo.previewChunks')}</span>
<RiSearchEyeLine className="size-4" />
<span className="px-0.5">{t('datasetPipeline.addDocuments.stepTwo.previewChunks')}</span>
</Button>
</div>
)

View File

@ -1,5 +1,5 @@
import { usePublishedPipelineProcessingParams } from '@/service/use-pipeline'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { usePublishedPipelineProcessingParams } from '@/service/use-pipeline'
export const useInputVariables = (datasourceNodeId: string) => {
const pipelineId = useDatasetDetailContextWithSelector(state => state.dataset?.pipeline_id)

View File

@ -1,10 +1,10 @@
import type { BaseConfiguration } from '@/app/components/base/form/form-scenarios/base/types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import React from 'react'
import ProcessDocuments from './index'
import type { BaseConfiguration } from '@/app/components/base/form/form-scenarios/base/types'
import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/types'
import { useInputVariables } from './hooks'
import { useConfigurations, useInitialData } from '@/app/components/rag-pipeline/hooks/use-input-fields'
import { useInputVariables } from './hooks'
import ProcessDocuments from './index'
// ==========================================
// Mock External Dependencies

View File

@ -1,9 +1,9 @@
import React from 'react'
import { generateZodSchema } from '@/app/components/base/form/form-scenarios/base/utils'
import { useInputVariables } from './hooks'
import Form from './form'
import Actions from './actions'
import { useConfigurations, useInitialData } from '@/app/components/rag-pipeline/hooks/use-input-fields'
import Actions from './actions'
import Form from './form'
import { useInputVariables } from './hooks'
type ProcessDocumentsProps = {
dataSourceNodeId: string
@ -30,7 +30,7 @@ const ProcessDocuments = ({
const schema = generateZodSchema(configurations)
return (
<div className='flex flex-col gap-y-4 pt-4'>
<div className="flex flex-col gap-y-4 pt-4">
<Form
ref={ref}
initialData={initialData}

View File

@ -1,12 +1,13 @@
import type { Mock } from 'vitest'
import type { DocumentIndexingStatus, IndexingStatusResponse } from '@/models/datasets'
import type { InitialDocumentDetail } from '@/models/pipeline'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import React from 'react'
import EmbeddingProcess from './index'
import type { DocumentIndexingStatus, IndexingStatusResponse } from '@/models/datasets'
import { DatasourceType, type InitialDocumentDetail } from '@/models/pipeline'
import { Plan } from '@/app/components/billing/type'
import { RETRIEVE_METHOD } from '@/types/app'
import { IndexingType } from '@/app/components/datasets/create/step-two'
import { DatasourceType } from '@/models/pipeline'
import { RETRIEVE_METHOD } from '@/types/app'
import EmbeddingProcess from './index'
// ==========================================
// Mock External Dependencies
@ -22,7 +23,7 @@ vi.mock('next/navigation', () => ({
// Mock next/link
vi.mock('next/link', () => ({
default: function MockLink({ children, href, ...props }: { children: React.ReactNode; href: string }) {
default: function MockLink({ children, href, ...props }: { children: React.ReactNode, href: string }) {
return <a href={href} {...props}>{children}</a>
},
}))

View File

@ -1,6 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import type { IndexingType } from '@/app/components/datasets/create/step-two'
import type { IndexingStatusResponse } from '@/models/datasets'
import type { InitialDocumentDetail } from '@/models/pipeline'
import type { RETRIEVE_METHOD } from '@/types/app'
import {
RiAedFill,
RiArrowRightLine,
@ -9,25 +10,25 @@ import {
RiLoader2Fill,
RiTerminalBoxLine,
} from '@remixicon/react'
import { cn } from '@/utils/classnames'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import React, { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import type { IndexingStatusResponse } from '@/models/datasets'
import Divider from '@/app/components/base/divider'
import NotionIcon from '@/app/components/base/notion-icon'
import Tooltip from '@/app/components/base/tooltip'
import PriorityLabel from '@/app/components/billing/priority-label'
import { Plan } from '@/app/components/billing/type'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import { useProviderContext } from '@/context/provider-context'
import Tooltip from '@/app/components/base/tooltip'
import { useInvalidDocumentList } from '@/service/knowledge/use-document'
import DocumentFileIcon from '@/app/components/datasets/common/document-file-icon'
import RuleDetail from './rule-detail'
import type { IndexingType } from '@/app/components/datasets/create/step-two'
import type { RETRIEVE_METHOD } from '@/types/app'
import { DatasourceType, type InitialDocumentDetail } from '@/models/pipeline'
import { useIndexingStatusBatch, useProcessRule } from '@/service/knowledge/use-dataset'
import Divider from '@/app/components/base/divider'
import { useProviderContext } from '@/context/provider-context'
import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url'
import Link from 'next/link'
import { DatasourceType } from '@/models/pipeline'
import { useIndexingStatusBatch, useProcessRule } from '@/service/knowledge/use-dataset'
import { useInvalidDocumentList } from '@/service/knowledge/use-document'
import { cn } from '@/utils/classnames'
import RuleDetail from './rule-detail'
type EmbeddingProcessProps = {
datasetId: string
@ -89,15 +90,18 @@ const EmbeddingProcess = ({
const apiReferenceUrl = useDatasetApiAccessUrl()
const isEmbeddingWaiting = useMemo(() => {
if (!indexingStatusBatchDetail.length) return false
if (!indexingStatusBatchDetail.length)
return false
return indexingStatusBatchDetail.every(indexingStatusDetail => ['waiting'].includes(indexingStatusDetail?.indexing_status || ''))
}, [indexingStatusBatchDetail])
const isEmbedding = useMemo(() => {
if (!indexingStatusBatchDetail.length) return false
if (!indexingStatusBatchDetail.length)
return false
return indexingStatusBatchDetail.some(indexingStatusDetail => ['indexing', 'splitting', 'parsing', 'cleaning'].includes(indexingStatusDetail?.indexing_status || ''))
}, [indexingStatusBatchDetail])
const isEmbeddingCompleted = useMemo(() => {
if (!indexingStatusBatchDetail.length) return false
if (!indexingStatusBatchDetail.length)
return false
return indexingStatusBatchDetail.every(indexingStatusDetail => ['completed', 'error', 'paused'].includes(indexingStatusDetail?.indexing_status || ''))
}, [indexingStatusBatchDetail])
@ -128,11 +132,11 @@ const EmbeddingProcess = ({
return (
<>
<div className='flex flex-col gap-y-3'>
<div className='system-md-semibold-uppercase flex items-center gap-x-1 text-text-secondary'>
<div className="flex flex-col gap-y-3">
<div className="system-md-semibold-uppercase flex items-center gap-x-1 text-text-secondary">
{(isEmbeddingWaiting || isEmbedding) && (
<>
<RiLoader2Fill className='size-4 animate-spin' />
<RiLoader2Fill className="size-4 animate-spin" />
<span>
{isEmbeddingWaiting ? t('datasetDocuments.embedding.waiting') : t('datasetDocuments.embedding.processing')}
</span>
@ -142,18 +146,18 @@ const EmbeddingProcess = ({
</div>
{
enableBilling && plan.type !== Plan.team && (
<div className='flex h-[52px] items-center gap-x-2 rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg p-2.5 pl-3 shadow-xs shadow-shadow-shadow-3'>
<div className='flex shrink-0 items-center justify-center rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-brand-blue-brand-500 shadow-md shadow-shadow-shadow-5'>
<RiAedFill className='size-4 text-text-primary-on-surface' />
<div className="flex h-[52px] items-center gap-x-2 rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg p-2.5 pl-3 shadow-xs shadow-shadow-shadow-3">
<div className="flex shrink-0 items-center justify-center rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-brand-blue-brand-500 shadow-md shadow-shadow-shadow-5">
<RiAedFill className="size-4 text-text-primary-on-surface" />
</div>
<div className='system-md-medium grow text-text-primary'>
<div className="system-md-medium grow text-text-primary">
{t('billing.plansCommon.documentProcessingPriorityUpgrade')}
</div>
<UpgradeBtn loc='knowledge-speed-up' />
<UpgradeBtn loc="knowledge-speed-up" />
</div>
)
}
<div className='flex flex-col gap-0.5 pb-2'>
<div className="flex flex-col gap-0.5 pb-2">
{indexingStatusBatchDetail.map(indexingStatusDetail => (
<div
key={indexingStatusDetail.id}
@ -164,84 +168,84 @@ const EmbeddingProcess = ({
>
{isSourceEmbedding(indexingStatusDetail) && (
<div
className='absolute left-0 top-0 h-full min-w-0.5 border-r-[2px] border-r-components-progress-bar-progress-highlight bg-components-progress-bar-progress'
className="absolute left-0 top-0 h-full min-w-0.5 border-r-[2px] border-r-components-progress-bar-progress-highlight bg-components-progress-bar-progress"
style={{ width: `${getSourcePercent(indexingStatusDetail)}%` }}
/>
)}
<div className='z-[1] flex h-full items-center gap-1 pl-[6px] pr-2'>
<div className="z-[1] flex h-full items-center gap-1 pl-[6px] pr-2">
{getSourceType(indexingStatusDetail.id) === DatasourceType.localFile && (
<DocumentFileIcon
size='sm'
className='shrink-0'
size="sm"
className="shrink-0"
name={getSourceName(indexingStatusDetail.id)}
extension={getFileType(getSourceName(indexingStatusDetail.id))}
/>
)}
{getSourceType(indexingStatusDetail.id) === DatasourceType.onlineDocument && (
<NotionIcon
className='shrink-0'
type='page'
className="shrink-0"
type="page"
src={getIcon(indexingStatusDetail.id)}
/>
)}
<div className='flex w-0 grow items-center gap-1' title={getSourceName(indexingStatusDetail.id)}>
<div className='system-xs-medium truncate text-text-secondary'>
<div className="flex w-0 grow items-center gap-1" title={getSourceName(indexingStatusDetail.id)}>
<div className="system-xs-medium truncate text-text-secondary">
{getSourceName(indexingStatusDetail.id)}
</div>
{
enableBilling && (
<PriorityLabel className='ml-0' />
<PriorityLabel className="ml-0" />
)
}
</div>
{isSourceEmbedding(indexingStatusDetail) && (
<div className='shrink-0 text-xs text-text-secondary'>{`${getSourcePercent(indexingStatusDetail)}%`}</div>
<div className="shrink-0 text-xs text-text-secondary">{`${getSourcePercent(indexingStatusDetail)}%`}</div>
)}
{indexingStatusDetail.indexing_status === 'error' && (
<Tooltip
popupClassName='px-4 py-[14px] max-w-60 body-xs-regular text-text-secondary border-[0.5px] border-components-panel-border rounded-xl'
popupClassName="px-4 py-[14px] max-w-60 body-xs-regular text-text-secondary border-[0.5px] border-components-panel-border rounded-xl"
offset={4}
popupContent={indexingStatusDetail.error}
>
<span>
<RiErrorWarningFill className='size-4 shrink-0 text-text-destructive' />
<RiErrorWarningFill className="size-4 shrink-0 text-text-destructive" />
</span>
</Tooltip>
)}
{indexingStatusDetail.indexing_status === 'completed' && (
<RiCheckboxCircleFill className='size-4 shrink-0 text-text-success' />
<RiCheckboxCircleFill className="size-4 shrink-0 text-text-success" />
)}
</div>
</div>
))}
</div>
<Divider type='horizontal' className='my-0 bg-divider-subtle' />
<Divider type="horizontal" className="my-0 bg-divider-subtle" />
<RuleDetail
sourceData={ruleDetail}
indexingType={indexingType}
retrievalMethod={retrievalMethod}
/>
</div>
<div className='mt-6 flex items-center gap-x-2 py-2'>
<div className="mt-6 flex items-center gap-x-2 py-2">
<Link
href={apiReferenceUrl}
target='_blank'
rel='noopener noreferrer'
target="_blank"
rel="noopener noreferrer"
>
<Button
className='w-fit gap-x-0.5 px-3'
className="w-fit gap-x-0.5 px-3"
>
<RiTerminalBoxLine className='size-4' />
<span className='px-0.5'>Access the API</span>
<RiTerminalBoxLine className="size-4" />
<span className="px-0.5">Access the API</span>
</Button>
</Link>
<Button
className='w-fit gap-x-0.5 px-3'
variant='primary'
className="w-fit gap-x-0.5 px-3"
variant="primary"
onClick={navToDocumentList}
>
<span className='px-0.5'>{t('datasetCreation.stepThree.navTo')}</span>
<RiArrowRightLine className='size-4 stroke-current stroke-1' />
<span className="px-0.5">{t('datasetCreation.stepThree.navTo')}</span>
<RiArrowRightLine className="size-4 stroke-current stroke-1" />
</Button>
</div>
</>

View File

@ -1,9 +1,10 @@
import type { ProcessRuleResponse } from '@/models/datasets'
import { render, screen } from '@testing-library/react'
import React from 'react'
import RuleDetail from './rule-detail'
import { ProcessMode, type ProcessRuleResponse } from '@/models/datasets'
import { RETRIEVE_METHOD } from '@/types/app'
import { IndexingType } from '@/app/components/datasets/create/step-two'
import { ProcessMode } from '@/models/datasets'
import { RETRIEVE_METHOD } from '@/types/app'
import RuleDetail from './rule-detail'
// ==========================================
// Mock External Dependencies
@ -12,15 +13,15 @@ import { IndexingType } from '@/app/components/datasets/create/step-two'
// Mock next/image (using img element for simplicity in tests)
vi.mock('next/image', () => ({
__esModule: true,
default: function MockImage({ src, alt, className }: { src: string; alt: string; className?: string }) {
// eslint-disable-next-line @next/next/no-img-element
default: function MockImage({ src, alt, className }: { src: string, alt: string, className?: string }) {
// eslint-disable-next-line next/no-img-element
return <img src={src} alt={alt} className={className} data-testid="next-image" />
},
}))
// Mock FieldInfo component
vi.mock('@/app/components/datasets/documents/detail/metadata', () => ({
FieldInfo: ({ label, displayedValue, valueIcon }: { label: string; displayedValue: string; valueIcon?: React.ReactNode }) => (
FieldInfo: ({ label, displayedValue, valueIcon }: { label: string, displayedValue: string, valueIcon?: React.ReactNode }) => (
<div data-testid="field-info" data-label={label}>
<span data-testid="field-label">{label}</span>
<span data-testid="field-value">{displayedValue}</span>

View File

@ -1,11 +1,12 @@
import React, { useCallback } from 'react'
import { IndexingType } from '@/app/components/datasets/create/step-two'
import { ProcessMode, type ProcessRuleResponse } from '@/models/datasets'
import { RETRIEVE_METHOD } from '@/types/app'
import { useTranslation } from 'react-i18next'
import { FieldInfo } from '@/app/components/datasets/documents/detail/metadata'
import type { ProcessRuleResponse } from '@/models/datasets'
import Image from 'next/image'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { indexMethodIcon, retrievalIcon } from '@/app/components/datasets/create/icons'
import { IndexingType } from '@/app/components/datasets/create/step-two'
import { FieldInfo } from '@/app/components/datasets/documents/detail/metadata'
import { ProcessMode } from '@/models/datasets'
import { RETRIEVE_METHOD } from '@/types/app'
type RuleDetailProps = {
sourceData?: ProcessRuleResponse
@ -39,7 +40,7 @@ const RuleDetail = ({
}, [sourceData, t])
return (
<div className='flex flex-col gap-1' data-testid='rule-detail'>
<div className="flex flex-col gap-1" data-testid="rule-detail">
<FieldInfo
label={t('datasetDocuments.embedding.mode')}
displayedValue={getValue('mode')}
@ -47,24 +48,24 @@ const RuleDetail = ({
<FieldInfo
label={t('datasetCreation.stepTwo.indexMode')}
displayedValue={t(`datasetCreation.stepTwo.${indexingType === IndexingType.ECONOMICAL ? 'economical' : 'qualified'}`) as string}
valueIcon={
valueIcon={(
<Image
className='size-4'
className="size-4"
src={
indexingType === IndexingType.ECONOMICAL
? indexMethodIcon.economical
: indexMethodIcon.high_quality
}
alt=''
alt=""
/>
}
)}
/>
<FieldInfo
label={t('datasetSettings.form.retrievalSetting.title')}
displayedValue={t(`dataset.retrieval.${indexingType === IndexingType.ECONOMICAL ? 'keyword_search' : retrievalMethod}.title`) as string}
valueIcon={
valueIcon={(
<Image
className='size-4'
className="size-4"
src={
retrievalMethod === RETRIEVE_METHOD.fullText
? retrievalIcon.fullText
@ -73,9 +74,9 @@ const RuleDetail = ({
? retrievalIcon.hybrid
: retrievalIcon.vector
}
alt=''
alt=""
/>
}
)}
/>
</div>
)

View File

@ -1,9 +1,9 @@
import type { DocumentIndexingStatus } from '@/models/datasets'
import type { InitialDocumentDetail } from '@/models/pipeline'
import { render, screen } from '@testing-library/react'
import React from 'react'
import Processing from './index'
import type { InitialDocumentDetail } from '@/models/pipeline'
import { DatasourceType } from '@/models/pipeline'
import type { DocumentIndexingStatus } from '@/models/datasets'
import Processing from './index'
// ==========================================
// Mock External Dependencies
@ -86,8 +86,7 @@ const createMockDocuments = (count: number): InitialDocumentDetail[] =>
id: `doc-${index + 1}`,
name: `document-${index + 1}.txt`,
position: index,
}),
)
}))
// ==========================================
// Test Suite

View File

@ -1,11 +1,11 @@
'use client'
import type { InitialDocumentDetail } from '@/models/pipeline'
import { RiBookOpenLine } from '@remixicon/react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { RiBookOpenLine } from '@remixicon/react'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useDocLink } from '@/context/i18n'
import EmbeddingProcess from './embedding-process'
import type { InitialDocumentDetail } from '@/models/pipeline'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
type ProcessingProps = {
batchId: string
@ -23,9 +23,9 @@ const Processing = ({
const retrievalMethod = useDatasetDetailContextWithSelector(s => s.dataset?.retrieval_model_dict?.search_method)
return (
<div className='flex h-full w-full justify-center overflow-hidden'>
<div className='h-full w-3/5 overflow-y-auto pb-8 pt-10'>
<div className='max-w-[640px]'>
<div className="flex h-full w-full justify-center overflow-hidden">
<div className="h-full w-3/5 overflow-y-auto pb-8 pt-10">
<div className="max-w-[640px]">
<EmbeddingProcess
datasetId={datasetId!}
batchId={batchId}
@ -35,19 +35,19 @@ const Processing = ({
/>
</div>
</div>
<div className='w-2/5 pr-8 pt-[88px]'>
<div className='flex w-[328px] flex-col gap-3 rounded-xl bg-background-section p-6'>
<div className='flex size-10 items-center justify-center rounded-[10px] bg-components-card-bg shadow-lg shadow-shadow-shadow-5'>
<RiBookOpenLine className='size-5 text-text-accent' />
<div className="w-2/5 pr-8 pt-[88px]">
<div className="flex w-[328px] flex-col gap-3 rounded-xl bg-background-section p-6">
<div className="flex size-10 items-center justify-center rounded-[10px] bg-components-card-bg shadow-lg shadow-shadow-shadow-5">
<RiBookOpenLine className="size-5 text-text-accent" />
</div>
<div className='flex flex-col gap-y-2'>
<div className='system-xl-semibold text-text-secondary'>{t('datasetCreation.stepThree.sideTipTitle')}</div>
<div className='system-sm-regular text-text-tertiary'>{t('datasetCreation.stepThree.sideTipContent')}</div>
<div className="flex flex-col gap-y-2">
<div className="system-xl-semibold text-text-secondary">{t('datasetCreation.stepThree.sideTipTitle')}</div>
<div className="system-sm-regular text-text-tertiary">{t('datasetCreation.stepThree.sideTipContent')}</div>
<a
href={docLink('/guides/knowledge-base/integrate-knowledge-within-application')}
target='_blank'
rel='noreferrer noopener'
className='system-sm-regular text-text-accent'
target="_blank"
rel="noreferrer noopener"
className="system-sm-regular text-text-accent"
>
{t('datasetPipeline.addDocuments.stepThree.learnMore')}
</a>

View File

@ -1,5 +1,5 @@
import { cn } from '@/utils/classnames'
import React from 'react'
import { cn } from '@/utils/classnames'
export type Step = {
label: string
@ -16,7 +16,7 @@ const StepIndicator = ({
steps,
}: StepIndicatorProps) => {
return (
<div className='flex gap-x-1'>
<div className="flex gap-x-1">
{steps.map((step, index) => {
const isActive = index === currentStep - 1
return (

View File

@ -1,15 +1,15 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import {
useCSVDownloader,
} from 'react-papaparse'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { Download02 as DownloadIcon } from '@/app/components/base/icons/src/vender/solid/general'
import { ChunkingMode } from '@/models/datasets'
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n-config/language'
import { ChunkingMode } from '@/models/datasets'
const CSV_TEMPLATE_QA_EN = [
['question', 'answer'],
@ -49,42 +49,66 @@ const CSVDownload: FC<{ docForm: ChunkingMode }> = ({ docForm }) => {
}
return (
<div className='mt-6'>
<div className='text-sm font-medium text-text-primary'>{t('share.generation.csvStructureTitle')}</div>
<div className='mt-2 max-h-[500px] overflow-auto'>
<div className="mt-6">
<div className="text-sm font-medium text-text-primary">{t('share.generation.csvStructureTitle')}</div>
<div className="mt-2 max-h-[500px] overflow-auto">
{docForm === ChunkingMode.qa && (
<table className='w-full table-fixed border-separate border-spacing-0 rounded-lg border border-divider-subtle text-xs'>
<thead className='text-text-secondary'>
<table className="w-full table-fixed border-separate border-spacing-0 rounded-lg border border-divider-subtle text-xs">
<thead className="text-text-secondary">
<tr>
<td className='h-9 border-b border-divider-subtle pl-3 pr-2'>{t('datasetDocuments.list.batchModal.question')}</td>
<td className='h-9 border-b border-divider-subtle pl-3 pr-2'>{t('datasetDocuments.list.batchModal.answer')}</td>
<td className="h-9 border-b border-divider-subtle pl-3 pr-2">{t('datasetDocuments.list.batchModal.question')}</td>
<td className="h-9 border-b border-divider-subtle pl-3 pr-2">{t('datasetDocuments.list.batchModal.answer')}</td>
</tr>
</thead>
<tbody className='text-text-tertiary'>
<tbody className="text-text-tertiary">
<tr>
<td className='h-9 border-b border-divider-subtle pl-3 pr-2 text-[13px]'>{t('datasetDocuments.list.batchModal.question')} 1</td>
<td className='h-9 border-b border-divider-subtle pl-3 pr-2 text-[13px]'>{t('datasetDocuments.list.batchModal.answer')} 1</td>
<td className="h-9 border-b border-divider-subtle pl-3 pr-2 text-[13px]">
{t('datasetDocuments.list.batchModal.question')}
{' '}
1
</td>
<td className="h-9 border-b border-divider-subtle pl-3 pr-2 text-[13px]">
{t('datasetDocuments.list.batchModal.answer')}
{' '}
1
</td>
</tr>
<tr>
<td className='h-9 pl-3 pr-2 text-[13px]'>{t('datasetDocuments.list.batchModal.question')} 2</td>
<td className='h-9 pl-3 pr-2 text-[13px]'>{t('datasetDocuments.list.batchModal.answer')} 2</td>
<td className="h-9 pl-3 pr-2 text-[13px]">
{t('datasetDocuments.list.batchModal.question')}
{' '}
2
</td>
<td className="h-9 pl-3 pr-2 text-[13px]">
{t('datasetDocuments.list.batchModal.answer')}
{' '}
2
</td>
</tr>
</tbody>
</table>
)}
{docForm === ChunkingMode.text && (
<table className='w-full table-fixed border-separate border-spacing-0 rounded-lg border border-divider-subtle text-xs'>
<thead className='text-text-secondary'>
<table className="w-full table-fixed border-separate border-spacing-0 rounded-lg border border-divider-subtle text-xs">
<thead className="text-text-secondary">
<tr>
<td className='h-9 border-b border-divider-subtle pl-3 pr-2'>{t('datasetDocuments.list.batchModal.contentTitle')}</td>
<td className="h-9 border-b border-divider-subtle pl-3 pr-2">{t('datasetDocuments.list.batchModal.contentTitle')}</td>
</tr>
</thead>
<tbody className='text-text-tertiary'>
<tbody className="text-text-tertiary">
<tr>
<td className='h-9 border-b border-divider-subtle pl-3 pr-2 text-[13px]'>{t('datasetDocuments.list.batchModal.content')} 1</td>
<td className="h-9 border-b border-divider-subtle pl-3 pr-2 text-[13px]">
{t('datasetDocuments.list.batchModal.content')}
{' '}
1
</td>
</tr>
<tr>
<td className='h-9 pl-3 pr-2 text-[13px]'>{t('datasetDocuments.list.batchModal.content')} 2</td>
<td className="h-9 pl-3 pr-2 text-[13px]">
{t('datasetDocuments.list.batchModal.content')}
{' '}
2
</td>
</tr>
</tbody>
</table>
@ -93,12 +117,12 @@ const CSVDownload: FC<{ docForm: ChunkingMode }> = ({ docForm }) => {
<CSVDownloader
className="mt-2 block cursor-pointer"
type={Type.Link}
filename={'template'}
filename="template"
bom={true}
data={getTemplate()}
>
<div className='flex h-[18px] items-center space-x-1 text-xs font-medium text-text-accent'>
<DownloadIcon className='mr-1 h-3 w-3' />
<div className="flex h-[18px] items-center space-x-1 text-xs font-medium text-text-accent">
<DownloadIcon className="mr-1 h-3 w-3" />
{t('datasetDocuments.list.batchModal.template')}
</div>
</CSVDownloader>

View File

@ -1,22 +1,22 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { FileItem } from '@/models/datasets'
import {
RiDeleteBinLine,
} from '@remixicon/react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { cn } from '@/utils/classnames'
import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files'
import { ToastContext } from '@/app/components/base/toast'
import Button from '@/app/components/base/button'
import type { FileItem } from '@/models/datasets'
import { upload } from '@/service/base'
import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils'
import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files'
import SimplePieChart from '@/app/components/base/simple-pie-chart'
import { Theme } from '@/types/app'
import { ToastContext } from '@/app/components/base/toast'
import useTheme from '@/hooks/use-theme'
import { upload } from '@/service/base'
import { useFileUploadConfig } from '@/service/use-common'
import { Theme } from '@/types/app'
import { cn } from '@/utils/classnames'
export type Props = {
file: FileItem | undefined
@ -192,46 +192,46 @@ const CSVUploader: FC<Props> = ({
}, [])
return (
<div className='mt-6'>
<div className="mt-6">
<input
ref={fileUploader}
style={{ display: 'none' }}
type="file"
id="fileUploader"
accept='.csv'
accept=".csv"
onChange={fileChangeHandle}
/>
<div ref={dropRef}>
{!file && (
<div className={cn('flex h-20 items-center rounded-xl border border-dashed border-components-panel-border bg-components-panel-bg-blur text-sm font-normal', dragging && 'border border-divider-subtle bg-components-panel-on-panel-item-bg-hover')}>
<div className='flex w-full items-center justify-center space-x-2'>
<div className="flex w-full items-center justify-center space-x-2">
<CSVIcon className="shrink-0" />
<div className='text-text-secondary'>
<div className="text-text-secondary">
{t('datasetDocuments.list.batchModal.csvUploadTitle')}
<span className='cursor-pointer text-text-accent' onClick={selectHandle}>{t('datasetDocuments.list.batchModal.browse')}</span>
<span className="cursor-pointer text-text-accent" onClick={selectHandle}>{t('datasetDocuments.list.batchModal.browse')}</span>
</div>
</div>
{dragging && <div ref={dragRef} className='absolute left-0 top-0 h-full w-full' />}
{dragging && <div ref={dragRef} className="absolute left-0 top-0 h-full w-full" />}
</div>
)}
{file && (
<div className={cn('group flex h-20 items-center rounded-xl border border-components-panel-border bg-components-panel-bg-blur px-6 text-sm font-normal', 'hover:border-divider-subtle hover:bg-components-panel-on-panel-item-bg-hover')}>
<CSVIcon className="shrink-0" />
<div className='ml-2 flex w-0 grow'>
<span className='max-w-[calc(100%_-_30px)] overflow-hidden text-ellipsis whitespace-nowrap text-text-primary'>{file.file.name.replace(/.csv$/, '')}</span>
<span className='shrink-0 text-text-secondary'>.csv</span>
<div className="ml-2 flex w-0 grow">
<span className="max-w-[calc(100%_-_30px)] overflow-hidden text-ellipsis whitespace-nowrap text-text-primary">{file.file.name.replace(/.csv$/, '')}</span>
<span className="shrink-0 text-text-secondary">.csv</span>
</div>
<div className='hidden items-center group-hover:flex'>
<div className="hidden items-center group-hover:flex">
{(file.progress < 100 && file.progress >= 0) && (
<>
<SimplePieChart percentage={file.progress} stroke={chartColor} fill={chartColor} animationDuration={0}/>
<div className='mx-2 h-4 w-px bg-text-secondary'/>
<SimplePieChart percentage={file.progress} stroke={chartColor} fill={chartColor} animationDuration={0} />
<div className="mx-2 h-4 w-px bg-text-secondary" />
</>
)}
<Button onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.change')}</Button>
<div className='mx-2 h-4 w-px bg-text-secondary' />
<div className='cursor-pointer p-2' onClick={removeFile}>
<RiDeleteBinLine className='h-4 w-4 text-text-secondary' />
<div className="mx-2 h-4 w-px bg-text-secondary" />
<div className="cursor-pointer p-2" onClick={removeFile}>
<RiDeleteBinLine className="h-4 w-4 text-text-secondary" />
</div>
</div>
</div>

View File

@ -1,14 +1,14 @@
'use client'
import type { FC } from 'react'
import type { ChunkingMode, FileItem } from '@/models/datasets'
import { RiCloseLine } from '@remixicon/react'
import { noop } from 'lodash-es'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiCloseLine } from '@remixicon/react'
import CSVUploader from './csv-uploader'
import CSVDownloader from './csv-downloader'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import type { ChunkingMode, FileItem } from '@/models/datasets'
import { noop } from 'lodash-es'
import CSVDownloader from './csv-downloader'
import CSVUploader from './csv-uploader'
export type IBatchModalProps = {
isShow: boolean
@ -40,10 +40,10 @@ const BatchModal: FC<IBatchModalProps> = ({
}, [isShow])
return (
<Modal isShow={isShow} onClose={noop} className='!max-w-[520px] !rounded-xl px-8 py-6'>
<div className='relative pb-1 text-xl font-medium leading-[30px] text-text-primary'>{t('datasetDocuments.list.batchModal.title')}</div>
<div className='absolute right-4 top-4 cursor-pointer p-2' onClick={onCancel}>
<RiCloseLine className='h-4 w-4 text-text-secondary' />
<Modal isShow={isShow} onClose={noop} className="!max-w-[520px] !rounded-xl px-8 py-6">
<div className="relative pb-1 text-xl font-medium leading-[30px] text-text-primary">{t('datasetDocuments.list.batchModal.title')}</div>
<div className="absolute right-4 top-4 cursor-pointer p-2" onClick={onCancel}>
<RiCloseLine className="h-4 w-4 text-text-secondary" />
</div>
<CSVUploader
file={currentCSV}
@ -52,8 +52,8 @@ const BatchModal: FC<IBatchModalProps> = ({
<CSVDownloader
docForm={docForm}
/>
<div className='mt-[28px] flex justify-end pt-6'>
<Button className='mr-2' onClick={onCancel}>
<div className="mt-[28px] flex justify-end pt-6">
<Button className="mr-2" onClick={onCancel}>
{t('datasetDocuments.list.batchModal.cancel')}
</Button>
<Button variant="primary" onClick={handleSend} disabled={!currentCSV || !currentCSV.file || !currentCSV.file.id}>

View File

@ -1,21 +1,22 @@
import React, { type FC, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { FC } from 'react'
import type { ChildChunkDetail, ChunkingMode } from '@/models/datasets'
import {
RiCloseLine,
RiCollapseDiagonalLine,
RiExpandDiagonalLine,
} from '@remixicon/react'
import React, { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { cn } from '@/utils/classnames'
import { formatNumber } from '@/utils/format'
import { formatTime } from '@/utils/time'
import ActionButtons from './common/action-buttons'
import ChunkContent from './common/chunk-content'
import Dot from './common/dot'
import { SegmentIndexTag } from './common/segment-index-tag'
import { useSegmentListContext } from './index'
import type { ChildChunkDetail, ChunkingMode } from '@/models/datasets'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { formatNumber } from '@/utils/format'
import { cn } from '@/utils/classnames'
import Divider from '@/app/components/base/divider'
import { formatTime } from '@/utils/time'
type IChildSegmentDetailProps = {
chunkId: string
@ -71,21 +72,21 @@ const ChildSegmentDetail: FC<IChildSegmentDetailProps> = ({
}, [childChunkInfo?.updated_at])
return (
<div className={'flex h-full flex-col'}>
<div className="flex h-full flex-col">
<div className={cn('flex items-center justify-between', fullScreen ? 'border border-divider-subtle py-3 pl-6 pr-4' : 'pl-4 pr-3 pt-3')}>
<div className='flex flex-col'>
<div className='system-xl-semibold text-text-primary'>{t('datasetDocuments.segment.editChildChunk')}</div>
<div className='flex items-center gap-x-2'>
<div className="flex flex-col">
<div className="system-xl-semibold text-text-primary">{t('datasetDocuments.segment.editChildChunk')}</div>
<div className="flex items-center gap-x-2">
<SegmentIndexTag positionId={childChunkInfo?.position || ''} labelPrefix={t('datasetDocuments.segment.childChunk') as string} />
<Dot />
<span className='system-xs-medium text-text-tertiary'>{wordCountText}</span>
<span className="system-xs-medium text-text-tertiary">{wordCountText}</span>
<Dot />
<span className='system-xs-medium text-text-tertiary'>
<span className="system-xs-medium text-text-tertiary">
{EditTimeText}
</span>
</div>
</div>
<div className='flex items-center'>
<div className="flex items-center">
{fullScreen && (
<>
<ActionButtons
@ -94,14 +95,14 @@ const ChildSegmentDetail: FC<IChildSegmentDetailProps> = ({
loading={loading}
isChildChunk={true}
/>
<Divider type='vertical' className='ml-4 mr-2 h-3.5 bg-divider-regular' />
<Divider type="vertical" className="ml-4 mr-2 h-3.5 bg-divider-regular" />
</>
)}
<div className='mr-1 flex h-8 w-8 cursor-pointer items-center justify-center p-1.5' onClick={toggleFullScreen}>
{fullScreen ? <RiCollapseDiagonalLine className='h-4 w-4 text-text-tertiary' /> : <RiExpandDiagonalLine className='h-4 w-4 text-text-tertiary' />}
<div className="mr-1 flex h-8 w-8 cursor-pointer items-center justify-center p-1.5" onClick={toggleFullScreen}>
{fullScreen ? <RiCollapseDiagonalLine className="h-4 w-4 text-text-tertiary" /> : <RiExpandDiagonalLine className="h-4 w-4 text-text-tertiary" />}
</div>
<div className='flex h-8 w-8 cursor-pointer items-center justify-center p-1.5' onClick={onCancel}>
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
<div className="flex h-8 w-8 cursor-pointer items-center justify-center p-1.5" onClick={onCancel}>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</div>
</div>
</div>
@ -116,7 +117,7 @@ const ChildSegmentDetail: FC<IChildSegmentDetailProps> = ({
</div>
</div>
{!fullScreen && (
<div className='flex items-center justify-end border-t-[1px] border-t-divider-subtle p-4 pt-3'>
<div className="flex items-center justify-end border-t-[1px] border-t-divider-subtle p-4 pt-3">
<ActionButtons
handleCancel={handleCancel}
handleSave={handleSave}

View File

@ -1,17 +1,18 @@
import { type FC, useMemo, useState } from 'react'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { EditSlice } from '../../../formatted-text/flavours/edit-slice'
import { useDocumentContext } from '../context'
import { FormattedText } from '../../../formatted-text/formatted'
import Empty from './common/empty'
import FullDocListSkeleton from './skeleton/full-doc-list-skeleton'
import { useSegmentListContext } from './index'
import type { FC } from 'react'
import type { ChildChunkDetail } from '@/models/datasets'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import Input from '@/app/components/base/input'
import { cn } from '@/utils/classnames'
import Divider from '@/app/components/base/divider'
import { formatNumber } from '@/utils/format'
import { EditSlice } from '../../../formatted-text/flavours/edit-slice'
import { FormattedText } from '../../../formatted-text/formatted'
import { useDocumentContext } from '../context'
import Empty from './common/empty'
import { useSegmentListContext } from './index'
import FullDocListSkeleton from './skeleton/full-doc-list-skeleton'
type IChildSegmentCardProps = {
childChunks: ChildChunkDetail[]
@ -69,8 +70,8 @@ const ChildSegmentList: FC<IChildSegmentCardProps> = ({
if (!isSearch) {
const text = isFullDocMode
? !total
? '--'
: formatNumber(total)
? '--'
: formatNumber(total)
: formatNumber(childChunks.length)
const count = isFullDocMode
? text === '--'
@ -92,8 +93,9 @@ const ChildSegmentList: FC<IChildSegmentCardProps> = ({
contentOpacity,
isParagraphMode ? 'pb-2 pt-1' : 'grow px-3',
(isFullDocMode && isLoading) && 'overflow-y-hidden',
)}>
{isFullDocMode ? <Divider type='horizontal' className='my-1 h-px bg-divider-subtle' /> : null}
)}
>
{isFullDocMode ? <Divider type="horizontal" className="my-1 h-px bg-divider-subtle" /> : null}
<div className={cn('flex items-center justify-between', isFullDocMode ? 'sticky -top-2 left-0 bg-background-default pb-3 pt-2' : '')}>
<div
className={cn(
@ -111,15 +113,15 @@ const ChildSegmentList: FC<IChildSegmentCardProps> = ({
isParagraphMode
? collapsed
? (
<RiArrowRightSLine className='mr-0.5 h-4 w-4 text-text-secondary opacity-50' />
)
: (<RiArrowDownSLine className='mr-0.5 h-4 w-4 text-text-secondary' />)
<RiArrowRightSLine className="mr-0.5 h-4 w-4 text-text-secondary opacity-50" />
)
: (<RiArrowDownSLine className="mr-0.5 h-4 w-4 text-text-secondary" />)
: null
}
<span className='system-sm-semibold-uppercase text-text-secondary'>{totalText}</span>
<span className="system-sm-semibold-uppercase text-text-secondary">{totalText}</span>
<span className={cn('pl-1.5 text-xs font-medium text-text-quaternary', isParagraphMode ? 'hidden group-hover/card:inline-block' : '')}>·</span>
<button
type='button'
type="button"
className={cn(
'system-xs-semibold-uppercase px-1.5 py-1 text-components-button-secondary-accent-text',
isParagraphMode ? 'hidden group-hover/card:inline-block' : '',
@ -135,59 +137,68 @@ const ChildSegmentList: FC<IChildSegmentCardProps> = ({
</button>
</div>
{isFullDocMode
? <Input
showLeftIcon
showClearIcon
wrapperClassName='!w-52'
value={inputValue}
onChange={e => handleInputChange?.(e.target.value)}
onClear={() => handleInputChange?.('')}
/>
? (
<Input
showLeftIcon
showClearIcon
wrapperClassName="!w-52"
value={inputValue}
onChange={e => handleInputChange?.(e.target.value)}
onClear={() => handleInputChange?.('')}
/>
)
: null}
</div>
{isLoading ? <FullDocListSkeleton /> : null}
{((isFullDocMode && !isLoading) || !collapsed)
? <div className={cn('flex gap-x-0.5', isFullDocMode ? 'mb-6 grow' : 'items-center')}>
{isParagraphMode && (
<div className='self-stretch'>
<Divider type='vertical' className='mx-[7px] w-[2px] bg-text-accent-secondary' />
? (
<div className={cn('flex gap-x-0.5', isFullDocMode ? 'mb-6 grow' : 'items-center')}>
{isParagraphMode && (
<div className="self-stretch">
<Divider type="vertical" className="mx-[7px] w-[2px] bg-text-accent-secondary" />
</div>
)}
{childChunks.length > 0
? (
<FormattedText className={cn('flex w-full flex-col !leading-6', isParagraphMode ? 'gap-y-2' : 'gap-y-3')}>
{childChunks.map((childChunk) => {
const edited = childChunk.updated_at !== childChunk.created_at
const focused = currChildChunk?.childChunkInfo?.id === childChunk.id
return (
<EditSlice
key={childChunk.id}
label={`C-${childChunk.position}${edited ? ` · ${t('datasetDocuments.segment.edited')}` : ''}`}
text={childChunk.content}
onDelete={() => onDelete?.(childChunk.segment_id, childChunk.id)}
className="child-chunk"
labelClassName={focused ? 'bg-state-accent-solid text-text-primary-on-surface' : ''}
labelInnerClassName="text-[10px] font-semibold align-bottom leading-6"
contentClassName={cn('!leading-6', focused ? 'bg-state-accent-hover-alt text-text-primary' : 'text-text-secondary')}
showDivider={false}
onClick={(e) => {
e.stopPropagation()
onClickSlice?.(childChunk)
}}
offsetOptions={({ rects }) => {
return {
mainAxis: isFullDocMode ? -rects.floating.width : 12 - rects.floating.width,
crossAxis: (20 - rects.floating.height) / 2,
}
}}
/>
)
})}
</FormattedText>
)
: inputValue !== ''
? (
<div className="h-full w-full">
<Empty onClearFilter={onClearFilter!} />
</div>
)
: null}
</div>
)}
{childChunks.length > 0
? <FormattedText className={cn('flex w-full flex-col !leading-6', isParagraphMode ? 'gap-y-2' : 'gap-y-3')}>
{childChunks.map((childChunk) => {
const edited = childChunk.updated_at !== childChunk.created_at
const focused = currChildChunk?.childChunkInfo?.id === childChunk.id
return <EditSlice
key={childChunk.id}
label={`C-${childChunk.position}${edited ? ` · ${t('datasetDocuments.segment.edited')}` : ''}`}
text={childChunk.content}
onDelete={() => onDelete?.(childChunk.segment_id, childChunk.id)}
className='child-chunk'
labelClassName={focused ? 'bg-state-accent-solid text-text-primary-on-surface' : ''}
labelInnerClassName={'text-[10px] font-semibold align-bottom leading-6'}
contentClassName={cn('!leading-6', focused ? 'bg-state-accent-hover-alt text-text-primary' : 'text-text-secondary')}
showDivider={false}
onClick={(e) => {
e.stopPropagation()
onClickSlice?.(childChunk)
}}
offsetOptions={({ rects }) => {
return {
mainAxis: isFullDocMode ? -rects.floating.width : 12 - rects.floating.width,
crossAxis: (20 - rects.floating.height) / 2,
}
}}
/>
})}
</FormattedText>
: inputValue !== ''
? <div className='h-full w-full'>
<Empty onClearFilter={onClearFilter!} />
</div>
: null
}
</div>
)
: null}
</div>
)

View File

@ -1,10 +1,11 @@
import React, { type FC, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import type { FC } from 'react'
import { useKeyPress } from 'ahooks'
import { useDocumentContext } from '../../context'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
import { ChunkingMode } from '@/models/datasets'
import { useDocumentContext } from '../../context'
type IActionButtonsProps = {
handleCancel: () => void
@ -39,44 +40,44 @@ const ActionButtons: FC<IActionButtonsProps> = ({
if (loading)
return
handleSave()
},
{ exactMatch: true, useCapture: true })
}, { exactMatch: true, useCapture: true })
const isParentChildParagraphMode = useMemo(() => {
return docForm === ChunkingMode.parentChild && parentMode === 'paragraph'
}, [docForm, parentMode])
return (
<div className='flex items-center gap-x-2'>
<div className="flex items-center gap-x-2">
<Button
onClick={handleCancel}
>
<div className='flex items-center gap-x-1'>
<span className='system-sm-medium text-components-button-secondary-text'>{t('common.operation.cancel')}</span>
<span className='system-kbd rounded-[4px] bg-components-kbd-bg-gray px-[1px] text-text-tertiary'>ESC</span>
<div className="flex items-center gap-x-1">
<span className="system-sm-medium text-components-button-secondary-text">{t('common.operation.cancel')}</span>
<span className="system-kbd rounded-[4px] bg-components-kbd-bg-gray px-[1px] text-text-tertiary">ESC</span>
</div>
</Button>
{(isParentChildParagraphMode && actionType === 'edit' && !isChildChunk && showRegenerationButton)
? <Button
onClick={handleRegeneration}
disabled={loading}
>
<span className='system-sm-medium text-components-button-secondary-text'>
{t('common.operation.saveAndRegenerate')}
</span>
</Button>
: null
}
? (
<Button
onClick={handleRegeneration}
disabled={loading}
>
<span className="system-sm-medium text-components-button-secondary-text">
{t('common.operation.saveAndRegenerate')}
</span>
</Button>
)
: null}
<Button
variant='primary'
variant="primary"
onClick={handleSave}
disabled={loading}
>
<div className='flex items-center gap-x-1'>
<span className='text-components-button-primary-text'>{t('common.operation.save')}</span>
<div className='flex items-center gap-x-0.5'>
<span className='system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white capitalize text-text-primary-on-surface'>{getKeyboardKeyNameBySystem('ctrl')}</span>
<span className='system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface'>S</span>
<div className="flex items-center gap-x-1">
<span className="text-components-button-primary-text">{t('common.operation.save')}</span>
<div className="flex items-center gap-x-0.5">
<span className="system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white capitalize text-text-primary-on-surface">{getKeyboardKeyNameBySystem('ctrl')}</span>
<span className="system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface">S</span>
</div>
</div>
</Button>

View File

@ -1,7 +1,8 @@
import React, { type FC } from 'react'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
import Checkbox from '@/app/components/base/checkbox'
import { cn } from '@/utils/classnames'
type AddAnotherProps = {
className?: string
@ -19,12 +20,12 @@ const AddAnother: FC<AddAnotherProps> = ({
return (
<div className={cn('flex items-center gap-x-1 pl-1', className)}>
<Checkbox
key='add-another-checkbox'
className='shrink-0'
key="add-another-checkbox"
className="shrink-0"
checked={isChecked}
onCheck={onCheck}
/>
<span className='system-xs-medium text-text-tertiary'>{t('datasetDocuments.segment.addAnother')}</span>
<span className="system-xs-medium text-text-tertiary">{t('datasetDocuments.segment.addAnother')}</span>
</div>
)
}

View File

@ -1,11 +1,12 @@
import React, { type FC } from 'react'
import type { FC } from 'react'
import { RiArchive2Line, RiCheckboxCircleLine, RiCloseCircleLine, RiDeleteBinLine, RiDraftLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import React from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Confirm from '@/app/components/base/confirm'
import Divider from '@/app/components/base/divider'
import { cn } from '@/utils/classnames'
import Confirm from '@/app/components/base/confirm'
import Button from '@/app/components/base/button'
const i18nPrefix = 'dataset.batchAction'
type IBatchActionProps = {
@ -45,68 +46,68 @@ const BatchAction: FC<IBatchActionProps> = ({
}
return (
<div className={cn('pointer-events-none flex w-full justify-center gap-x-2', className)}>
<div className='pointer-events-auto flex items-center gap-x-1 rounded-[10px] border border-components-actionbar-border-accent bg-components-actionbar-bg-accent p-1 shadow-xl shadow-shadow-shadow-5'>
<div className='inline-flex items-center gap-x-2 py-1 pl-2 pr-3'>
<span className='system-xs-medium flex h-5 w-5 items-center justify-center rounded-md bg-text-accent text-text-primary-on-surface'>
<div className="pointer-events-auto flex items-center gap-x-1 rounded-[10px] border border-components-actionbar-border-accent bg-components-actionbar-bg-accent p-1 shadow-xl shadow-shadow-shadow-5">
<div className="inline-flex items-center gap-x-2 py-1 pl-2 pr-3">
<span className="system-xs-medium flex h-5 w-5 items-center justify-center rounded-md bg-text-accent text-text-primary-on-surface">
{selectedIds.length}
</span>
<span className='system-sm-semibold text-text-accent'>{t(`${i18nPrefix}.selected`)}</span>
<span className="system-sm-semibold text-text-accent">{t(`${i18nPrefix}.selected`)}</span>
</div>
<Divider type='vertical' className='mx-0.5 h-3.5 bg-divider-regular' />
<Divider type="vertical" className="mx-0.5 h-3.5 bg-divider-regular" />
<Button
variant='ghost'
className='gap-x-0.5 px-3'
variant="ghost"
className="gap-x-0.5 px-3"
onClick={onBatchEnable}
>
<RiCheckboxCircleLine className='size-4' />
<span className='px-0.5'>{t(`${i18nPrefix}.enable`)}</span>
<RiCheckboxCircleLine className="size-4" />
<span className="px-0.5">{t(`${i18nPrefix}.enable`)}</span>
</Button>
<Button
variant='ghost'
className='gap-x-0.5 px-3'
variant="ghost"
className="gap-x-0.5 px-3"
onClick={onBatchDisable}
>
<RiCloseCircleLine className='size-4' />
<span className='px-0.5'>{t(`${i18nPrefix}.disable`)}</span>
<RiCloseCircleLine className="size-4" />
<span className="px-0.5">{t(`${i18nPrefix}.disable`)}</span>
</Button>
{onEditMetadata && (
<Button
variant='ghost'
className='gap-x-0.5 px-3'
variant="ghost"
className="gap-x-0.5 px-3"
onClick={onEditMetadata}
>
<RiDraftLine className='size-4' />
<span className='px-0.5'>{t('dataset.metadata.metadata')}</span>
<RiDraftLine className="size-4" />
<span className="px-0.5">{t('dataset.metadata.metadata')}</span>
</Button>
)}
{onArchive && (
<Button
variant='ghost'
className='gap-x-0.5 px-3'
variant="ghost"
className="gap-x-0.5 px-3"
onClick={onArchive}
>
<RiArchive2Line className='size-4' />
<span className='px-0.5'>{t(`${i18nPrefix}.archive`)}</span>
<RiArchive2Line className="size-4" />
<span className="px-0.5">{t(`${i18nPrefix}.archive`)}</span>
</Button>
)}
<Button
variant='ghost'
variant="ghost"
destructive
className='gap-x-0.5 px-3'
className="gap-x-0.5 px-3"
onClick={showDeleteConfirm}
>
<RiDeleteBinLine className='size-4' />
<span className='px-0.5'>{t(`${i18nPrefix}.delete`)}</span>
<RiDeleteBinLine className="size-4" />
<span className="px-0.5">{t(`${i18nPrefix}.delete`)}</span>
</Button>
<Divider type='vertical' className='mx-0.5 h-3.5 bg-divider-regular' />
<Divider type="vertical" className="mx-0.5 h-3.5 bg-divider-regular" />
<Button
variant='ghost'
className='px-3'
variant="ghost"
className="px-3"
onClick={onCancel}
>
<span className='px-0.5'>{t(`${i18nPrefix}.cancel`)}</span>
<span className="px-0.5">{t(`${i18nPrefix}.cancel`)}</span>
</Button>
</div>
{

View File

@ -1,9 +1,9 @@
import React, { useEffect, useRef, useState } from 'react'
import type { ComponentProps, FC } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Markdown } from '@/app/components/base/markdown'
import { ChunkingMode } from '@/models/datasets'
import { cn } from '@/utils/classnames'
import { Markdown } from '@/app/components/base/markdown'
type IContentProps = ComponentProps<'textarea'>
@ -16,8 +16,7 @@ const Textarea: FC<IContentProps> = React.memo(({
}) => {
return (
<textarea
className={cn('inset-0 w-full resize-none appearance-none overflow-y-auto border-none bg-transparent outline-none',
className)}
className={cn('inset-0 w-full resize-none appearance-none overflow-y-auto border-none bg-transparent outline-none', className)}
placeholder={placeholder}
value={value}
disabled={disabled}
@ -80,8 +79,7 @@ const AutoResizeTextArea: FC<IAutoResizeTextAreaProps> = React.memo(({
return (
<textarea
ref={textareaRef}
className={cn('inset-0 w-full resize-none appearance-none border-none bg-transparent outline-none',
className)}
className={cn('inset-0 w-full resize-none appearance-none border-none bg-transparent outline-none', className)}
style={{
maxHeight,
}}
@ -115,10 +113,10 @@ const QATextArea: FC<IQATextAreaProps> = React.memo(({
const labelRef = useRef<HTMLDivElement>(null)
return (
<div ref={containerRef} className='h-full overflow-hidden'>
<div ref={labelRef} className='mb-1 text-xs font-medium text-text-tertiary'>QUESTION</div>
<div ref={containerRef} className="h-full overflow-hidden">
<div ref={labelRef} className="mb-1 text-xs font-medium text-text-tertiary">QUESTION</div>
<AutoResizeTextArea
className='text-sm tracking-[-0.07px] text-text-secondary caret-[#295EFF]'
className="text-sm tracking-[-0.07px] text-text-secondary caret-[#295EFF]"
value={question}
placeholder={t('datasetDocuments.segment.questionPlaceholder') || ''}
onChange={e => onQuestionChange(e.target.value)}
@ -126,9 +124,9 @@ const QATextArea: FC<IQATextAreaProps> = React.memo(({
containerRef={containerRef}
labelRef={labelRef}
/>
<div className='mb-1 mt-6 text-xs font-medium text-text-tertiary'>ANSWER</div>
<div className="mb-1 mt-6 text-xs font-medium text-text-tertiary">ANSWER</div>
<AutoResizeTextArea
className='text-sm tracking-[-0.07px] text-text-secondary caret-[#295EFF]'
className="text-sm tracking-[-0.07px] text-text-secondary caret-[#295EFF]"
value={answer}
placeholder={t('datasetDocuments.segment.answerPlaceholder') || ''}
onChange={e => onAnswerChange?.(e.target.value)}
@ -163,19 +161,21 @@ const ChunkContent: FC<IChunkContentProps> = ({
const { t } = useTranslation()
if (docForm === ChunkingMode.qa) {
return <QATextArea
question={question}
answer={answer}
onQuestionChange={onQuestionChange}
onAnswerChange={onAnswerChange}
isEditMode={isEditMode}
/>
return (
<QATextArea
question={question}
answer={answer}
onQuestionChange={onQuestionChange}
onAnswerChange={onAnswerChange}
isEditMode={isEditMode}
/>
)
}
if (!isEditMode) {
return (
<Markdown
className='h-full w-full !text-text-secondary'
className="h-full w-full !text-text-secondary"
content={question}
customDisallowedElements={['input']}
/>
@ -184,7 +184,7 @@ const ChunkContent: FC<IChunkContentProps> = ({
return (
<Textarea
className='body-md-regular h-full w-full pb-6 tracking-[-0.07px] text-text-secondary caret-[#295EFF]'
className="body-md-regular h-full w-full pb-6 tracking-[-0.07px] text-text-secondary caret-[#295EFF]"
value={question}
placeholder={t('datasetDocuments.segment.contentPlaceholder') || ''}
onChange={e => onQuestionChange(e.target.value)}

View File

@ -2,7 +2,7 @@ import React from 'react'
const Dot = () => {
return (
<div className='system-xs-medium text-text-quaternary'>·</div>
<div className="system-xs-medium text-text-quaternary">·</div>
)
}

View File

@ -1,7 +1,7 @@
import { useKeyPress } from 'ahooks'
import React, { useCallback, useEffect, useRef } from 'react'
import { createPortal } from 'react-dom'
import { cn } from '@/utils/classnames'
import { useKeyPress } from 'ahooks'
import { useSegmentListContext } from '..'
type DrawerProps = {
@ -32,14 +32,16 @@ const Drawer = ({
const currChildChunk = useSegmentListContext(s => s.currChildChunk)
useKeyPress('esc', (e) => {
if (!open) return
if (!open)
return
e.preventDefault()
onClose()
}, { exactMatch: true, useCapture: true })
const shouldCloseDrawer = useCallback((target: Node | null) => {
const panelContent = panelContentRef.current
if (!panelContent) return false
if (!panelContent)
return false
const chunks = document.querySelectorAll('.chunk-card')
const childChunks = document.querySelectorAll('.child-chunk')
const imagePreviewer = document.querySelector('.image-previewer')
@ -56,9 +58,11 @@ const Drawer = ({
}, [currSegment, currChildChunk, needCheckChunks])
const onDownCapture = useCallback((e: PointerEvent) => {
if (!open || modal) return
if (!open || modal)
return
const panelContent = panelContentRef.current
if (!panelContent) return
if (!panelContent)
return
const target = e.target as Node | null
if (shouldCloseDrawer(target))
queueMicrotask(onClose)
@ -73,22 +77,24 @@ const Drawer = ({
const isHorizontal = side === 'left' || side === 'right'
const content = (
<div className='pointer-events-none fixed inset-0 z-[9999]'>
{showOverlay ? (
<div
onClick={modal ? onClose : undefined}
aria-hidden='true'
className={cn(
'fixed inset-0 bg-black/30 opacity-0 transition-opacity duration-200 ease-in',
open && 'opacity-100',
modal && open ? 'pointer-events-auto' : 'pointer-events-none',
)}
/>
) : null}
<div className="pointer-events-none fixed inset-0 z-[9999]">
{showOverlay
? (
<div
onClick={modal ? onClose : undefined}
aria-hidden="true"
className={cn(
'fixed inset-0 bg-black/30 opacity-0 transition-opacity duration-200 ease-in',
open && 'opacity-100',
modal && open ? 'pointer-events-auto' : 'pointer-events-none',
)}
/>
)
: null}
{/* Drawer panel */}
<div
role='dialog'
role="dialog"
aria-modal={modal ? 'true' : 'false'}
className={cn(
'pointer-events-auto fixed flex flex-col',

View File

@ -1,5 +1,6 @@
import React, { type FC } from 'react'
import type { FC } from 'react'
import { RiFileList2Line } from '@remixicon/react'
import React from 'react'
import { useTranslation } from 'react-i18next'
type IEmptyProps = {
@ -8,7 +9,7 @@ type IEmptyProps = {
const EmptyCard = React.memo(() => {
return (
<div className='h-32 w-full shrink-0 rounded-xl bg-background-section-burn opacity-30' />
<div className="h-32 w-full shrink-0 rounded-xl bg-background-section-burn opacity-30" />
)
})
@ -22,13 +23,13 @@ const Line = React.memo(({
className,
}: LineProps) => {
return (
<svg xmlns='http://www.w3.org/2000/svg' width='2' height='241' viewBox='0 0 2 241' fill='none' className={className}>
<path d='M1 0.5L1 240.5' stroke='url(#paint0_linear_1989_74474)' />
<svg xmlns="http://www.w3.org/2000/svg" width="2" height="241" viewBox="0 0 2 241" fill="none" className={className}>
<path d="M1 0.5L1 240.5" stroke="url(#paint0_linear_1989_74474)" />
<defs>
<linearGradient id='paint0_linear_1989_74474' x1='-7.99584' y1='240.5' x2='-7.88094' y2='0.50004' gradientUnits='userSpaceOnUse'>
<stop stopColor='white' stopOpacity='0.01' />
<stop offset='0.503965' stopColor='#101828' stopOpacity='0.08' />
<stop offset='1' stopColor='white' stopOpacity='0.01' />
<linearGradient id="paint0_linear_1989_74474" x1="-7.99584" y1="240.5" x2="-7.88094" y2="0.50004" gradientUnits="userSpaceOnUse">
<stop stopColor="white" stopOpacity="0.01" />
<stop offset="0.503965" stopColor="#101828" stopOpacity="0.08" />
<stop offset="1" stopColor="white" stopOpacity="0.01" />
</linearGradient>
</defs>
</svg>
@ -43,34 +44,34 @@ const Empty: FC<IEmptyProps> = ({
const { t } = useTranslation()
return (
<div className={'relative z-0 flex h-full items-center justify-center'}>
<div className='flex flex-col items-center'>
<div className='relative z-10 flex h-14 w-14 items-center justify-center rounded-xl border border-divider-subtle bg-components-card-bg shadow-lg shadow-shadow-shadow-5'>
<RiFileList2Line className='h-6 w-6 text-text-secondary' />
<Line className='absolute -right-px top-1/2 -translate-y-1/2' />
<Line className='absolute -left-px top-1/2 -translate-y-1/2' />
<Line className='absolute left-1/2 top-0 -translate-x-1/2 -translate-y-1/2 rotate-90' />
<Line className='absolute left-1/2 top-full -translate-x-1/2 -translate-y-1/2 rotate-90' />
<div className="relative z-0 flex h-full items-center justify-center">
<div className="flex flex-col items-center">
<div className="relative z-10 flex h-14 w-14 items-center justify-center rounded-xl border border-divider-subtle bg-components-card-bg shadow-lg shadow-shadow-shadow-5">
<RiFileList2Line className="h-6 w-6 text-text-secondary" />
<Line className="absolute -right-px top-1/2 -translate-y-1/2" />
<Line className="absolute -left-px top-1/2 -translate-y-1/2" />
<Line className="absolute left-1/2 top-0 -translate-x-1/2 -translate-y-1/2 rotate-90" />
<Line className="absolute left-1/2 top-full -translate-x-1/2 -translate-y-1/2 rotate-90" />
</div>
<div className='system-md-regular mt-3 text-text-tertiary'>
<div className="system-md-regular mt-3 text-text-tertiary">
{t('datasetDocuments.segment.empty')}
</div>
<button
type='button'
className='system-sm-medium mt-1 text-text-accent'
type="button"
className="system-sm-medium mt-1 text-text-accent"
onClick={onClearFilter}
>
{t('datasetDocuments.segment.clearFilter')}
</button>
</div>
<div className='absolute left-0 top-0 -z-20 flex h-full w-full flex-col gap-y-3 overflow-hidden'>
<div className="absolute left-0 top-0 -z-20 flex h-full w-full flex-col gap-y-3 overflow-hidden">
{
Array.from({ length: 10 }).map((_, i) => (
<EmptyCard key={i} />
))
}
</div>
<div className='absolute left-0 top-0 -z-10 h-full w-full bg-dataset-chunk-list-mask-bg' />
<div className="absolute left-0 top-0 -z-10 h-full w-full bg-dataset-chunk-list-mask-bg" />
</div>
)
}

View File

@ -1,7 +1,7 @@
import React from 'react'
import Drawer from './drawer'
import { cn } from '@/utils/classnames'
import { noop } from 'lodash-es'
import React from 'react'
import { cn } from '@/utils/classnames'
import Drawer from './drawer'
type IFullScreenDrawerProps = {
isOpen: boolean
@ -39,7 +39,8 @@ const FullScreenDrawer = ({
modal={modal}
>
{children}
</Drawer>)
</Drawer>
)
}
export default FullScreenDrawer

View File

@ -1,8 +1,9 @@
import React, { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
import type { FC } from 'react'
import type { SegmentDetailModel } from '@/models/datasets'
import React from 'react'
import { useTranslation } from 'react-i18next'
import TagInput from '@/app/components/base/tag-input'
import { cn } from '@/utils/classnames'
type IKeywordsProps = {
segInfo?: Partial<SegmentDetailModel> & { id: string }
@ -24,19 +25,18 @@ const Keywords: FC<IKeywordsProps> = ({
const { t } = useTranslation()
return (
<div className={cn('flex flex-col', className)}>
<div className='system-xs-medium-uppercase text-text-tertiary'>{t('datasetDocuments.segment.keywords')}</div>
<div className='flex max-h-[200px] w-full flex-wrap gap-1 overflow-auto text-text-tertiary'>
<div className="system-xs-medium-uppercase text-text-tertiary">{t('datasetDocuments.segment.keywords')}</div>
<div className="flex max-h-[200px] w-full flex-wrap gap-1 overflow-auto text-text-tertiary">
{(!segInfo?.keywords?.length && actionType === 'view')
? '-'
: (
<TagInput
items={keywords}
onChange={newKeywords => onKeywordsChange(newKeywords)}
disableAdd={!isEditMode}
disableRemove={!isEditMode || (keywords.length === 1)}
/>
)
}
<TagInput
items={keywords}
onChange={newKeywords => onKeywordsChange(newKeywords)}
disableAdd={!isEditMode}
disableRemove={!isEditMode || (keywords.length === 1)}
/>
)}
</div>
</div>
)

View File

@ -1,11 +1,12 @@
import React, { type FC, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { FC } from 'react'
import { RiLoader2Line } from '@remixicon/react'
import { useCountDown } from 'ahooks'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { noop } from 'lodash-es'
import React, { useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import { useEventEmitterContextContext } from '@/context/event-emitter'
type IDefaultContentProps = {
onCancel: () => void
@ -20,15 +21,15 @@ const DefaultContent: FC<IDefaultContentProps> = React.memo(({
return (
<>
<div className='pb-4'>
<span className='title-2xl-semi-bold text-text-primary'>{t('datasetDocuments.segment.regenerationConfirmTitle')}</span>
<p className='system-md-regular text-text-secondary'>{t('datasetDocuments.segment.regenerationConfirmMessage')}</p>
<div className="pb-4">
<span className="title-2xl-semi-bold text-text-primary">{t('datasetDocuments.segment.regenerationConfirmTitle')}</span>
<p className="system-md-regular text-text-secondary">{t('datasetDocuments.segment.regenerationConfirmMessage')}</p>
</div>
<div className='flex justify-end gap-x-2 pt-6'>
<div className="flex justify-end gap-x-2 pt-6">
<Button onClick={onCancel}>
{t('common.operation.cancel')}
</Button>
<Button variant='warning' destructive onClick={onConfirm}>
<Button variant="warning" destructive onClick={onConfirm}>
{t('common.operation.regenerate')}
</Button>
</div>
@ -43,13 +44,13 @@ const RegeneratingContent: FC = React.memo(() => {
return (
<>
<div className='pb-4'>
<span className='title-2xl-semi-bold text-text-primary'>{t('datasetDocuments.segment.regeneratingTitle')}</span>
<p className='system-md-regular text-text-secondary'>{t('datasetDocuments.segment.regeneratingMessage')}</p>
<div className="pb-4">
<span className="title-2xl-semi-bold text-text-primary">{t('datasetDocuments.segment.regeneratingTitle')}</span>
<p className="system-md-regular text-text-secondary">{t('datasetDocuments.segment.regeneratingMessage')}</p>
</div>
<div className='flex justify-end pt-6'>
<Button variant='warning' destructive disabled className='inline-flex items-center gap-x-0.5'>
<RiLoader2Line className='h-4 w-4 animate-spin text-components-button-destructive-primary-text-disabled' />
<div className="flex justify-end pt-6">
<Button variant="warning" destructive disabled className="inline-flex items-center gap-x-0.5">
<RiLoader2Line className="h-4 w-4 animate-spin text-components-button-destructive-primary-text-disabled" />
<span>{t('common.operation.regenerate')}</span>
</Button>
</div>
@ -77,12 +78,12 @@ const RegenerationCompletedContent: FC<IRegenerationCompletedContentProps> = Rea
return (
<>
<div className='pb-4'>
<span className='title-2xl-semi-bold text-text-primary'>{t('datasetDocuments.segment.regenerationSuccessTitle')}</span>
<p className='system-md-regular text-text-secondary'>{t('datasetDocuments.segment.regenerationSuccessMessage')}</p>
<div className="pb-4">
<span className="title-2xl-semi-bold text-text-primary">{t('datasetDocuments.segment.regenerationSuccessTitle')}</span>
<p className="system-md-regular text-text-secondary">{t('datasetDocuments.segment.regenerationSuccessMessage')}</p>
</div>
<div className='flex justify-end pt-6'>
<Button variant='primary' onClick={onClose}>
<div className="flex justify-end pt-6">
<Button variant="primary" onClick={onClose}>
{`${t('common.operation.close')}${countdown === 0 ? '' : `(${Math.round(countdown / 1000)})`}`}
</Button>
</div>
@ -121,7 +122,7 @@ const RegenerationModal: FC<IRegenerationModalProps> = ({
})
return (
<Modal isShow={isShow} onClose={noop} className='!max-w-[480px] !rounded-2xl' wrapperClassName='!z-[10000]'>
<Modal isShow={isShow} onClose={noop} className="!max-w-[480px] !rounded-2xl" wrapperClassName="!z-[10000]">
{!loading && !updateSucceeded && <DefaultContent onCancel={onCancel} onConfirm={onConfirm} />}
{loading && !updateSucceeded && <RegeneratingContent />}
{!loading && updateSucceeded && <RegenerationCompletedContent onClose={onClose} />}

View File

@ -1,4 +1,5 @@
import React, { type FC, useMemo } from 'react'
import type { FC } from 'react'
import React, { useMemo } from 'react'
import { Chunk } from '@/app/components/base/icons/src/vender/knowledge'
import { cn } from '@/utils/classnames'

View File

@ -1,11 +1,11 @@
import React from 'react'
import { cn } from '@/utils/classnames'
const Tag = ({ text, className }: { text: string; className?: string }) => {
const Tag = ({ text, className }: { text: string, className?: string }) => {
return (
<div className={cn('inline-flex items-center gap-x-0.5', className)}>
<span className='text-xs font-medium text-text-quaternary'>#</span>
<span className='max-w-12 shrink-0 truncate text-xs text-text-tertiary'>{text}</span>
<span className="text-xs font-medium text-text-quaternary">#</span>
<span className="max-w-12 shrink-0 truncate text-xs text-text-tertiary">{text}</span>
</div>
)
}

View File

@ -1,8 +1,9 @@
import React, { type FC } from 'react'
import { useTranslation } from 'react-i18next'
import type { FC } from 'react'
import { RiLineHeight } from '@remixicon/react'
import Tooltip from '@/app/components/base/tooltip'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Collapse } from '@/app/components/base/icons/src/vender/knowledge'
import Tooltip from '@/app/components/base/tooltip'
type DisplayToggleProps = {
isCollapsed: boolean
@ -18,18 +19,18 @@ const DisplayToggle: FC<DisplayToggleProps> = ({
return (
<Tooltip
popupContent={isCollapsed ? t('datasetDocuments.segment.expandChunks') : t('datasetDocuments.segment.collapseChunks')}
popupClassName='text-text-secondary system-xs-medium border-[0.5px] border-components-panel-border'
popupClassName="text-text-secondary system-xs-medium border-[0.5px] border-components-panel-border"
>
<button
type='button'
className='flex items-center justify-center rounded-lg border-[0.5px] border-components-button-secondary-border
bg-components-button-secondary-bg p-2 shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px]'
type="button"
className="flex items-center justify-center rounded-lg border-[0.5px] border-components-button-secondary-border
bg-components-button-secondary-bg p-2 shadow-xs shadow-shadow-shadow-3 backdrop-blur-[5px]"
onClick={toggleCollapsed}
>
{
isCollapsed
? <RiLineHeight className='h-4 w-4 text-components-button-secondary-text' />
: <Collapse className='h-4 w-4 text-components-button-secondary-text' />
? <RiLineHeight className="h-4 w-4 text-components-button-secondary-text" />
: <Collapse className="h-4 w-4 text-components-button-secondary-text" />
}
</button>

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