mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 09:58:04 +08:00
chore(web): new lint setup (#30020)
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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})`,
|
||||
}}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = (
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'
|
||||
|
||||
// ==========================================
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(' / ')}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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'))
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
} => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}`,
|
||||
}),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
},
|
||||
}))
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
{
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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} />}
|
||||
|
||||
@ -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'
|
||||
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
Reference in New Issue
Block a user