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

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

View File

@ -1,7 +1,11 @@
import type { FC } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import type {
DataSourceInfo,
FullDocumentDetail,
IndexingStatusResponse,
LegacyDataSourceInfo,
ProcessRuleResponse,
} from '@/models/datasets'
import {
RiArrowRightLine,
RiCheckboxCircleFill,
@ -10,35 +14,31 @@ import {
RiTerminalBoxLine,
} from '@remixicon/react'
import Image from 'next/image'
import { indexMethodIcon, retrievalIcon } from '../icons'
import { IndexingType } from '../step-two'
import DocumentFileIcon from '../../common/document-file-icon'
import { cn } from '@/utils/classnames'
import { FieldInfo } from '@/app/components/datasets/documents/detail/metadata'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import type {
DataSourceInfo,
FullDocumentDetail,
IndexingStatusResponse,
LegacyDataSourceInfo,
ProcessRuleResponse,
} from '@/models/datasets'
import { fetchIndexingStatusBatch as doFetchIndexingStatus } from '@/service/datasets'
import { DataSourceType, ProcessMode } from '@/models/datasets'
import Divider from '@/app/components/base/divider'
import { ZapFast } from '@/app/components/base/icons/src/vender/solid/general'
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 { ZapFast } from '@/app/components/base/icons/src/vender/solid/general'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import { FieldInfo } from '@/app/components/datasets/documents/detail/metadata'
import { useProviderContext } from '@/context/provider-context'
import { sleep } from '@/utils'
import { RETRIEVE_METHOD } from '@/types/app'
import Tooltip from '@/app/components/base/tooltip'
import { useInvalidDocumentList } from '@/service/knowledge/use-document'
import Divider from '@/app/components/base/divider'
import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url'
import Link from 'next/link'
import { DataSourceType, ProcessMode } from '@/models/datasets'
import { fetchIndexingStatusBatch as doFetchIndexingStatus } from '@/service/datasets'
import { useProcessRule } from '@/service/knowledge/use-dataset'
import { useInvalidDocumentList } from '@/service/knowledge/use-document'
import { RETRIEVE_METHOD } from '@/types/app'
import { sleep } from '@/utils'
import { cn } from '@/utils/classnames'
import DocumentFileIcon from '../../common/document-file-icon'
import { indexMethodIcon, retrievalIcon } from '../icons'
import { IndexingType } from '../step-two'
type Props = {
datasetId: string
@ -105,54 +105,58 @@ const RuleDetail: FC<{
value = !sourceData?.mode
? value
: sourceData?.rules?.pre_processing_rules?.filter(rule =>
rule.enabled).map(rule => getRuleName(rule.id)).join(',')
rule.enabled).map(rule => getRuleName(rule.id)).join(',')
break
}
return value
}, [sourceData])
return <div className='flex flex-col gap-1'>
{Object.keys(segmentationRuleMap).map((field) => {
return <FieldInfo
key={field}
label={segmentationRuleMap[field as keyof typeof segmentationRuleMap]}
displayedValue={String(getValue(field))}
return (
<div className="flex flex-col gap-1">
{Object.keys(segmentationRuleMap).map((field) => {
return (
<FieldInfo
key={field}
label={segmentationRuleMap[field as keyof typeof segmentationRuleMap]}
displayedValue={String(getValue(field))}
/>
)
})}
<FieldInfo
label={t('datasetCreation.stepTwo.indexMode')}
displayedValue={t(`datasetCreation.stepTwo.${indexingType === IndexingType.ECONOMICAL ? 'economical' : 'qualified'}`) as string}
valueIcon={(
<Image
className="size-4"
src={
indexingType === IndexingType.ECONOMICAL
? indexMethodIcon.economical
: indexMethodIcon.high_quality
}
alt=""
/>
)}
/>
})}
<FieldInfo
label={t('datasetCreation.stepTwo.indexMode')}
displayedValue={t(`datasetCreation.stepTwo.${indexingType === IndexingType.ECONOMICAL ? 'economical' : 'qualified'}`) as string}
valueIcon={
<Image
className='size-4'
src={
indexingType === IndexingType.ECONOMICAL
? indexMethodIcon.economical
: indexMethodIcon.high_quality
}
alt=''
/>
}
/>
<FieldInfo
label={t('datasetSettings.form.retrievalSetting.title')}
// displayedValue={t(`datasetSettings.form.retrievalSetting.${retrievalMethod}`) as string}
displayedValue={t(`dataset.retrieval.${indexingType === IndexingType.ECONOMICAL ? 'keyword_search' : retrievalMethod}.title`) as string}
valueIcon={
<Image
className='size-4'
src={
retrievalMethod === RETRIEVE_METHOD.fullText
? retrievalIcon.fullText
: retrievalMethod === RETRIEVE_METHOD.hybrid
? retrievalIcon.hybrid
: retrievalIcon.vector
}
alt=''
/>
}
/>
</div>
<FieldInfo
label={t('datasetSettings.form.retrievalSetting.title')}
// displayedValue={t(`datasetSettings.form.retrievalSetting.${retrievalMethod}`) as string}
displayedValue={t(`dataset.retrieval.${indexingType === IndexingType.ECONOMICAL ? 'keyword_search' : retrievalMethod}.title`) as string}
valueIcon={(
<Image
className="size-4"
src={
retrievalMethod === RETRIEVE_METHOD.fullText
? retrievalIcon.fullText
: retrievalMethod === RETRIEVE_METHOD.hybrid
? retrievalIcon.hybrid
: retrievalIcon.vector
}
alt=""
/>
)}
/>
</div>
)
}
const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], indexingType, retrievalMethod }) => {
@ -257,11 +261,11 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
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">
{isEmbedding && (
<>
<RiLoader2Fill className='size-4 animate-spin' />
<RiLoader2Fill className="size-4 animate-spin" />
<span>{t('datasetDocuments.embedding.processing')}</span>
</>
)}
@ -269,18 +273,18 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
</div>
{
enableBilling && plan.type !== Plan.team && (
<div className='flex h-14 items-center rounded-xl border-[0.5px] border-black/5 bg-white p-3 shadow-md'>
<div className='flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-[#FFF6ED]'>
<ZapFast className='h-4 w-4 text-[#FB6514]' />
<div className="flex h-14 items-center rounded-xl border-[0.5px] border-black/5 bg-white p-3 shadow-md">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-[#FFF6ED]">
<ZapFast className="h-4 w-4 text-[#FB6514]" />
</div>
<div className='mx-3 grow text-[13px] font-medium text-gray-700'>
<div className="mx-3 grow text-[13px] font-medium text-gray-700">
{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}
@ -291,84 +295,84 @@ const EmbeddingProcess: FC<Props> = ({ datasetId, batchId, documents = [], index
>
{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.FILE && (
<DocumentFileIcon
size='sm'
className='shrink-0'
size="sm"
className="shrink-0"
name={getSourceName(indexingStatusDetail.id)}
extension={getFileType(getSourceName(indexingStatusDetail.id))}
/>
)}
{getSourceType(indexingStatusDetail.id) === DataSourceType.NOTION && (
<NotionIcon
className='shrink-0'
type='page'
className="shrink-0"
type="page"
src={getIcon(indexingStatusDetail.id)}
/>
)}
<div className='flex w-0 grow items-center gap-1' title={getSourceName(indexingStatusDetail.id)}>
<div className='system-xs-medium truncate text-text-secondary'>
<div className="flex w-0 grow items-center gap-1" title={getSourceName(indexingStatusDetail.id)}>
<div className="system-xs-medium truncate text-text-secondary">
{getSourceName(indexingStatusDetail.id)}
</div>
{
enableBilling && (
<PriorityLabel className='ml-0' />
<PriorityLabel className="ml-0" />
)
}
</div>
{isSourceEmbedding(indexingStatusDetail) && (
<div className='shrink-0 text-xs text-text-secondary'>{`${getSourcePercent(indexingStatusDetail)}%`}</div>
<div className="shrink-0 text-xs text-text-secondary">{`${getSourcePercent(indexingStatusDetail)}%`}</div>
)}
{indexingStatusDetail.indexing_status === 'error' && (
<Tooltip
popupClassName='px-4 py-[14px] max-w-60 body-xs-regular text-text-secondary border-[0.5px] border-components-panel-border rounded-xl'
popupClassName="px-4 py-[14px] max-w-60 body-xs-regular text-text-secondary border-[0.5px] border-components-panel-border rounded-xl"
offset={4}
popupContent={indexingStatusDetail.error}
>
<span>
<RiErrorWarningFill className='size-4 shrink-0 text-text-destructive' />
<RiErrorWarningFill className="size-4 shrink-0 text-text-destructive" />
</span>
</Tooltip>
)}
{indexingStatusDetail.indexing_status === 'completed' && (
<RiCheckboxCircleFill className='size-4 shrink-0 text-text-success' />
<RiCheckboxCircleFill className="size-4 shrink-0 text-text-success" />
)}
</div>
</div>
))}
</div>
<Divider type='horizontal' className='my-0 bg-divider-subtle' />
<Divider type="horizontal" className="my-0 bg-divider-subtle" />
<RuleDetail
sourceData={ruleDetail}
indexingType={indexingType}
retrievalMethod={retrievalMethod}
/>
</div>
<div className='mt-6 flex items-center gap-x-2 py-2'>
<div className="mt-6 flex items-center gap-x-2 py-2">
<Link
href={apiReferenceUrl}
target='_blank'
rel='noopener noreferrer'
target="_blank"
rel="noopener noreferrer"
>
<Button
className='w-fit gap-x-0.5 px-3'
className="w-fit gap-x-0.5 px-3"
>
<RiTerminalBoxLine className='size-4' />
<span className='px-0.5'>Access the API</span>
<RiTerminalBoxLine className="size-4" />
<span className="px-0.5">Access the API</span>
</Button>
</Link>
<Button
className='w-fit gap-x-0.5 px-3'
variant='primary'
className="w-fit gap-x-0.5 px-3"
variant="primary"
onClick={navToDocumentList}
>
<span className='px-0.5'>{t('datasetCreation.stepThree.navTo')}</span>
<RiArrowRightLine className='size-4 stroke-current stroke-1' />
<span className="px-0.5">{t('datasetCreation.stepThree.navTo')}</span>
<RiArrowRightLine className="size-4 stroke-current stroke-1" />
</Button>
</div>
</>

View File

@ -1,9 +1,9 @@
import type { MockedFunction } from 'vitest'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import React from 'react'
import EmptyDatasetCreationModal from './index'
import { createEmptyDataset } from '@/service/datasets'
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
import EmptyDatasetCreationModal from './index'
// Mock Next.js router
const mockPush = vi.fn()
@ -38,7 +38,7 @@ const mockInvalidDatasetList = vi.fn()
const mockUseInvalidDatasetList = useInvalidDatasetList as MockedFunction<typeof useInvalidDatasetList>
// Test data builder for props
const createDefaultProps = (overrides?: Partial<{ show: boolean; onHide: () => void }>) => ({
const createDefaultProps = (overrides?: Partial<{ show: boolean, onHide: () => void }>) => ({
show: true,
onHide: vi.fn(),
...overrides,

View File

@ -1,18 +1,18 @@
'use client'
import React, { useState } from 'react'
import { useRouter } from 'next/navigation'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import s from './index.module.css'
import { cn } from '@/utils/classnames'
import Modal from '@/app/components/base/modal'
import Input from '@/app/components/base/input'
import { trackEvent } from '@/app/components/base/amplitude'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import Modal from '@/app/components/base/modal'
import { ToastContext } from '@/app/components/base/toast'
import { createEmptyDataset } from '@/service/datasets'
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
import { trackEvent } from '@/app/components/base/amplitude'
import { cn } from '@/utils/classnames'
import s from './index.module.css'
type IProps = {
show: boolean
@ -68,9 +68,9 @@ const EmptyDatasetCreationModal = ({
<div className={s.label}>{t('datasetCreation.stepOne.modal.input')}</div>
<Input value={inputValue} placeholder={t('datasetCreation.stepOne.modal.placeholder') || ''} onChange={e => setInputValue(e.target.value)} />
</div>
<div className='flex flex-row-reverse'>
<Button className='ml-2 w-24' variant='primary' onClick={submit}>{t('datasetCreation.stepOne.modal.confirmButton')}</Button>
<Button className='w-24' onClick={onHide}>{t('datasetCreation.stepOne.modal.cancelButton')}</Button>
<div className="flex flex-row-reverse">
<Button className="ml-2 w-24" variant="primary" onClick={submit}>{t('datasetCreation.stepOne.modal.confirmButton')}</Button>
<Button className="w-24" onClick={onHide}>{t('datasetCreation.stepOne.modal.cancelButton')}</Button>
</div>
</Modal>
)

View File

@ -1,8 +1,8 @@
import type { MockedFunction } from 'vitest'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import FilePreview from './index'
import type { CustomFile as File } from '@/models/datasets'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { fetchFilePreview } from '@/service/common'
import FilePreview from './index'
// Mock the fetchFilePreview service
vi.mock('@/service/common', () => ({
@ -31,7 +31,7 @@ const createMockFile = (overrides: Partial<File> = {}): File => {
}
// Helper to render FilePreview with default props
const renderFilePreview = (props: Partial<{ file?: File; hidePreview: () => void }> = {}) => {
const renderFilePreview = (props: Partial<{ file?: File, hidePreview: () => void }> = {}) => {
const defaultProps = {
file: createMockFile(),
hidePreview: vi.fn(),
@ -705,8 +705,7 @@ describe('FilePreview', () => {
it('should handle rapid file changes', async () => {
// Arrange
const files = Array.from({ length: 5 }, (_, i) =>
createMockFile({ id: `file-${i}` }),
)
createMockFile({ id: `file-${i}` }))
// Act
const { rerender } = render(

View File

@ -1,12 +1,12 @@
'use client'
import type { CustomFile as File } from '@/models/datasets'
import { XMarkIcon } from '@heroicons/react/20/solid'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { XMarkIcon } from '@heroicons/react/20/solid'
import Loading from '@/app/components/base/loading'
import s from './index.module.css'
import { cn } from '@/utils/classnames'
import type { CustomFile as File } from '@/models/datasets'
import { fetchFilePreview } from '@/service/common'
import { cn } from '@/utils/classnames'
import s from './index.module.css'
type IProps = {
file?: File
@ -49,16 +49,20 @@ const FilePreview = ({
<div className={cn(s.previewHeader)}>
<div className={cn(s.title, 'title-md-semi-bold')}>
<span>{t('datasetCreation.stepOne.filePreview')}</span>
<div className='flex h-6 w-6 cursor-pointer items-center justify-center' onClick={hidePreview}>
<XMarkIcon className='h-4 w-4'></XMarkIcon>
<div className="flex h-6 w-6 cursor-pointer items-center justify-center" onClick={hidePreview}>
<XMarkIcon className="h-4 w-4"></XMarkIcon>
</div>
</div>
<div className={cn(s.fileName, 'system-xs-medium')}>
<span>{getFileName(file)}</span><span className={cn(s.filetype)}>.{file?.extension}</span>
<span>{getFileName(file)}</span>
<span className={cn(s.filetype)}>
.
{file?.extension}
</span>
</div>
</div>
<div className={cn(s.previewContent)}>
{loading && <Loading type='area' />}
{loading && <Loading type="area" />}
{!loading && (
<div className={cn(s.fileContent, 'body-md-regular')}>{previewContent}</div>
)}

View File

@ -1,22 +1,22 @@
'use client'
import type { CustomFile as File, FileItem } from '@/models/datasets'
import { RiDeleteBinLine, RiUploadCloud2Line } from '@remixicon/react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { RiDeleteBinLine, RiUploadCloud2Line } from '@remixicon/react'
import DocumentFileIcon from '../../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 { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils'
import SimplePieChart from '@/app/components/base/simple-pie-chart'
import { ToastContext } from '@/app/components/base/toast'
import { IS_CE_EDITION } from '@/config'
import I18n from '@/context/i18n'
import useTheme from '@/hooks/use-theme'
import { LanguagesSupported } from '@/i18n-config/language'
import { upload } from '@/service/base'
import { useFileSupportTypes, useFileUploadConfig } from '@/service/use-common'
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n-config/language'
import { IS_CE_EDITION } from '@/config'
import { Theme } from '@/types/app'
import useTheme from '@/hooks/use-theme'
import { getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils'
import { cn } from '@/utils/classnames'
import DocumentFileIcon from '../../common/document-file-icon'
type IFileUploaderProps = {
fileList: FileItem[]
@ -244,17 +244,20 @@ const FileUploader = ({
e.preventDefault()
e.stopPropagation()
setDragging(false)
if (!e.dataTransfer) return
if (!e.dataTransfer)
return
const nested = await Promise.all(
Array.from(e.dataTransfer.items).map((it) => {
const entry = (it as any).webkitGetAsEntry?.()
if (entry) return traverseFileEntry(entry)
if (entry)
return traverseFileEntry(entry)
const f = it.getAsFile?.()
return f ? Promise.resolve([f]) : Promise.resolve([])
}),
)
let files = nested.flat()
if (!supportBatchUpload) files = files.slice(0, 1)
if (!supportBatchUpload)
files = files.slice(0, 1)
files = files.slice(0, fileUploadConfig.batch_count_limit)
const valid = files.filter(isValid)
initialUpload(valid)
@ -314,7 +317,7 @@ const FileUploader = ({
{!hideUpload && (
<div ref={dropRef} className={cn('relative mb-2 box-border flex min-h-20 max-w-[640px] 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' />
<RiUploadCloud2Line className="mr-2 size-5" />
<span>
{supportBatchUpload ? t('datasetCreation.stepOne.uploader.button') : t('datasetCreation.stepOne.uploader.buttonSingleFile')}
@ -323,16 +326,18 @@ const FileUploader = ({
)}
</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>
)}
<div className='max-w-[640px] cursor-default space-y-1'>
<div className="max-w-[640px] cursor-default space-y-1">
{fileList.map((fileItem, index) => (
<div
@ -345,19 +350,19 @@ const FileUploader = ({
>
<div className="flex w-12 shrink-0 items-center justify-center">
<DocumentFileIcon
size='xl'
size="xl"
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="flex w-full">
<div className="w-0 grow truncate text-sm leading-4 text-text-secondary">{fileItem.file.name}</div>
</div>
<div className="w-full truncate leading-3 text-text-tertiary">
<span className='uppercase'>{getFileType(fileItem.file)}</span>
<span className='px-1 text-text-quaternary'>·</span>
<span className="uppercase">{getFileType(fileItem.file)}</span>
<span className="px-1 text-text-quaternary">·</span>
<span>{getFileSize(fileItem.file.size)}</span>
{/* <span className='px-1 text-text-quaternary'>·</span>
<span>10k characters</span> */}
@ -371,11 +376,14 @@ const FileUploader = ({
// <div className={s.percent}>{`${fileItem.progress}%`}</div>
<SimplePieChart percentage={fileItem.progress} stroke={chartColor} fill={chartColor} animationDuration={0} />
)}
<span className="flex h-6 w-6 cursor-pointer items-center justify-center" onClick={(e) => {
e.stopPropagation()
removeFile(fileItem.fileID)
}}>
<RiDeleteBinLine className='size-4 text-text-tertiary' />
<span
className="flex h-6 w-6 cursor-pointer items-center justify-center"
onClick={(e) => {
e.stopPropagation()
removeFile(fileItem.fileID)
}}
>
<RiDeleteBinLine className="size-4 text-text-tertiary" />
</span>
</div>
</div>

View File

@ -1,8 +1,8 @@
import GoldIcon from './assets/gold.svg'
import Piggybank from './assets/piggy-bank-mod.svg'
import Selection from './assets/selection-mod.svg'
import Research from './assets/research-mod.svg'
import PatternRecognition from './assets/pattern-recognition-mod.svg'
import Piggybank from './assets/piggy-bank-mod.svg'
import Research from './assets/research-mod.svg'
import Selection from './assets/selection-mod.svg'
export const indexMethodIcon = {
high_quality: GoldIcon,

View File

@ -1,11 +1,11 @@
import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types'
import type { DataSet } from '@/models/datasets'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import React from 'react'
import DatasetUpdateForm from './index'
import { ChunkingMode, DataSourceType, DatasetPermission } from '@/models/datasets'
import type { DataSet } from '@/models/datasets'
import { DataSourceProvider } from '@/models/common'
import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types'
import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets'
import { RETRIEVE_METHOD } from '@/types/app'
import DatasetUpdateForm from './index'
// IndexingType values from step-two (defined here since we mock step-two)
// Using type assertion to match the expected IndexingType enum from step-two
@ -27,7 +27,7 @@ vi.mock('react-i18next', () => ({
// Mock next/link
vi.mock('next/link', () => {
return function MockLink({ children, href }: { children: React.ReactNode; href: string }) {
return function MockLink({ children, href }: { children: React.ReactNode, href: string }) {
return <a href={href}>{children}</a>
}
})
@ -55,7 +55,7 @@ vi.mock('@/context/dataset-detail', () => ({
}))
// Mock useDefaultModel hook
let mockEmbeddingsDefaultModel: { model: string; provider: string } | undefined
let mockEmbeddingsDefaultModel: { model: string, provider: string } | undefined
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
useDefaultModel: () => ({
data: mockEmbeddingsDefaultModel,

View File

@ -1,22 +1,23 @@
'use client'
import type { NotionPage } from '@/models/common'
import type { CrawlOptions, CrawlResultItem, createDocumentResponse, FileItem } from '@/models/datasets'
import { produce } from 'immer'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useModalContextSelector } from '@/context/modal-context'
import { DataSourceProvider } from '@/models/common'
import { DataSourceType } from '@/models/datasets'
import { useGetDefaultDataSourceListAuth } from '@/service/use-datasource'
import AppUnavailable from '../../base/app-unavailable'
import { ModelTypeEnum } from '../../header/account-setting/model-provider-page/declarations'
import StepOne from './step-one'
import StepTwo from './step-two'
import StepThree from './step-three'
import StepTwo from './step-two'
import { TopBar } from './top-bar'
import { DataSourceType } from '@/models/datasets'
import type { CrawlOptions, CrawlResultItem, FileItem, createDocumentResponse } from '@/models/datasets'
import { DataSourceProvider, type NotionPage } from '@/models/common'
import { useModalContextSelector } from '@/context/modal-context'
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { useGetDefaultDataSourceListAuth } from '@/service/use-datasource'
import { produce } from 'immer'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import Loading from '@/app/components/base/loading'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
type DatasetUpdateFormProps = {
datasetId?: string
@ -104,12 +105,12 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
return <AppUnavailable code={500} unknownReason={t('datasetCreation.error.unavailable') as string} />
return (
<div className='flex flex-col overflow-hidden bg-components-panel-bg' style={{ height: 'calc(100vh - 56px)' }}>
<div className="flex flex-col overflow-hidden bg-components-panel-bg" style={{ height: 'calc(100vh - 56px)' }}>
<TopBar activeIndex={step - 1} datasetId={datasetId} />
<div style={{ height: 'calc(100% - 52px)' }}>
{
isLoadingAuthedDataSourceList && (
<Loading type='app' />
<Loading type="app" />
)
}
{

View File

@ -1,8 +1,8 @@
import type { MockedFunction } from 'vitest'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import NotionPagePreview from './index'
import type { NotionPage } from '@/models/common'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { fetchNotionPagePreview } from '@/service/datasets'
import NotionPagePreview from './index'
// Mock the fetchNotionPagePreview service
vi.mock('@/service/datasets', () => ({
@ -808,8 +808,7 @@ describe('NotionPagePreview', () => {
it('should handle rapid page changes', async () => {
// Arrange
const pages = Array.from({ length: 5 }, (_, i) =>
createMockNotionPage({ page_id: `page-${i}` }),
)
createMockNotionPage({ page_id: `page-${i}` }))
// Act
const { rerender } = render(

View File

@ -1,13 +1,13 @@
'use client'
import type { NotionPage } from '@/models/common'
import { XMarkIcon } from '@heroicons/react/20/solid'
import React, { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { XMarkIcon } from '@heroicons/react/20/solid'
import Loading from '@/app/components/base/loading'
import s from './index.module.css'
import { cn } from '@/utils/classnames'
import type { NotionPage } from '@/models/common'
import NotionIcon from '@/app/components/base/notion-icon'
import { fetchNotionPagePreview } from '@/service/datasets'
import { cn } from '@/utils/classnames'
import s from './index.module.css'
type IProps = {
currentPage?: NotionPage
@ -51,21 +51,21 @@ const NotionPagePreview = ({
<div className={cn(s.previewHeader)}>
<div className={cn(s.title, 'title-md-semi-bold')}>
<span>{t('datasetCreation.stepOne.pagePreview')}</span>
<div className='flex h-6 w-6 cursor-pointer items-center justify-center' onClick={hidePreview}>
<XMarkIcon className='h-4 w-4'></XMarkIcon>
<div className="flex h-6 w-6 cursor-pointer items-center justify-center" onClick={hidePreview}>
<XMarkIcon className="h-4 w-4"></XMarkIcon>
</div>
</div>
<div className={cn(s.fileName, 'system-xs-medium')}>
<NotionIcon
className='mr-1 shrink-0'
type='page'
className="mr-1 shrink-0"
type="page"
src={currentPage?.page_icon}
/>
{currentPage?.page_name}
</div>
</div>
<div className={cn(s.previewContent, 'body-md-regular')}>
{loading && <Loading type='area' />}
{loading && <Loading type="area" />}
{!loading && (
<div className={cn(s.fileContent, 'body-md-regular')}>{previewContent}</div>
)}

View File

@ -1,29 +1,29 @@
'use client'
import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types'
import type { DataSourceProvider, NotionPage } from '@/models/common'
import type { CrawlOptions, CrawlResultItem, FileItem } from '@/models/datasets'
import { RiArrowRightLine, RiFolder6Line } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowRightLine, RiFolder6Line } from '@remixicon/react'
import Button from '@/app/components/base/button'
import NotionConnector from '@/app/components/base/notion-connector'
import { NotionPageSelector } from '@/app/components/base/notion-page-selector'
import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
import { Plan } from '@/app/components/billing/type'
import VectorSpaceFull from '@/app/components/billing/vector-space-full'
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useProviderContext } from '@/context/provider-context'
import { DataSourceType } from '@/models/datasets'
import { cn } from '@/utils/classnames'
import EmptyDatasetCreationModal from '../empty-dataset-creation-modal'
import FilePreview from '../file-preview'
import FileUploader from '../file-uploader'
import NotionPagePreview from '../notion-page-preview'
import EmptyDatasetCreationModal from '../empty-dataset-creation-modal'
import Website from '../website'
import WebsitePreview from '../website/preview'
import s from './index.module.css'
import type { CrawlOptions, CrawlResultItem, FileItem } from '@/models/datasets'
import type { DataSourceProvider, NotionPage } from '@/models/common'
import { DataSourceType } from '@/models/datasets'
import Button from '@/app/components/base/button'
import { NotionPageSelector } from '@/app/components/base/notion-page-selector'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useProviderContext } from '@/context/provider-context'
import VectorSpaceFull from '@/app/components/billing/vector-space-full'
import { cn } from '@/utils/classnames'
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
import NotionConnector from '@/app/components/base/notion-connector'
import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types'
import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
import { useBoolean } from 'ahooks'
import { Plan } from '@/app/components/billing/type'
import UpgradeCard from './upgrade-card'
type IStepOneProps = {
@ -149,9 +149,11 @@ const StepOne = ({
}, [files, isShowVectorSpaceFull])
const isNotionAuthed = useMemo(() => {
if (!authedDataSourceList) return false
if (!authedDataSourceList)
return false
const notionSource = authedDataSourceList.find(item => item.provider === 'notion_datasource')
if (!notionSource) return false
if (!notionSource)
return false
return notionSource.credentials_list.length > 0
}, [authedDataSourceList])
@ -160,10 +162,10 @@ const StepOne = ({
}, [authedDataSourceList])
return (
<div className='h-full w-full overflow-x-auto'>
<div className='flex h-full w-full min-w-[1440px]'>
<div className='relative h-full w-1/2 overflow-y-auto'>
<div className='flex justify-end'>
<div className="h-full w-full overflow-x-auto">
<div className="flex h-full w-full min-w-[1440px]">
<div className="relative h-full w-1/2 overflow-y-auto">
<div className="flex justify-end">
<div className={cn(s.form)}>
{
shouldShowDataSourceTypeList && (
@ -174,7 +176,7 @@ const StepOne = ({
}
{
shouldShowDataSourceTypeList && (
<div className='mb-8 grid grid-cols-3 gap-4'>
<div className="mb-8 grid grid-cols-3 gap-4">
<div
className={cn(
s.dataSourceItem,
@ -193,7 +195,7 @@ const StepOne = ({
<span className={cn(s.datasetIcon)} />
<span
title={t('datasetCreation.stepOne.dataSourceType.file')!}
className='truncate'
className="truncate"
>
{t('datasetCreation.stepOne.dataSourceType.file')}
</span>
@ -216,7 +218,7 @@ const StepOne = ({
<span className={cn(s.datasetIcon, s.notion)} />
<span
title={t('datasetCreation.stepOne.dataSourceType.notion')!}
className='truncate'
className="truncate"
>
{t('datasetCreation.stepOne.dataSourceType.notion')}
</span>
@ -240,7 +242,7 @@ const StepOne = ({
<span className={cn(s.datasetIcon, s.web)} />
<span
title={t('datasetCreation.stepOne.dataSourceType.web')!}
className='truncate'
className="truncate"
>
{t('datasetCreation.stepOne.dataSourceType.web')}
</span>
@ -261,12 +263,12 @@ const StepOne = ({
supportBatchUpload={supportBatchUpload}
/>
{isShowVectorSpaceFull && (
<div className='mb-4 max-w-[640px]'>
<div className="mb-4 max-w-[640px]">
<VectorSpaceFull />
</div>
)}
<div className="flex max-w-[640px] justify-end gap-2">
<Button disabled={nextDisabled} variant='primary' onClick={onStepChange}>
<Button disabled={nextDisabled} variant="primary" onClick={onStepChange}>
<span className="flex gap-0.5 px-[10px]">
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
<RiArrowRightLine className="size-4" />
@ -275,8 +277,8 @@ const StepOne = ({
</div>
{
enableBilling && plan.type === Plan.sandbox && files.length > 0 && (
<div className='mt-5'>
<div className='mb-4 h-px bg-divider-subtle'></div>
<div className="mt-5">
<div className="mb-4 h-px bg-divider-subtle"></div>
<UpgradeCard />
</div>
)
@ -288,7 +290,7 @@ const StepOne = ({
{!isNotionAuthed && <NotionConnector onSetting={onSetting} />}
{isNotionAuthed && (
<>
<div className='mb-8 w-[640px]'>
<div className="mb-8 w-[640px]">
<NotionPageSelector
value={notionPages.map(page => page.page_id)}
onSelect={updateNotionPages}
@ -299,12 +301,12 @@ const StepOne = ({
/>
</div>
{isShowVectorSpaceFull && (
<div className='mb-4 max-w-[640px]'>
<div className="mb-4 max-w-[640px]">
<VectorSpaceFull />
</div>
)}
<div className="flex max-w-[640px] justify-end gap-2">
<Button disabled={isShowVectorSpaceFull || !notionPages.length} variant='primary' onClick={onStepChange}>
<Button disabled={isShowVectorSpaceFull || !notionPages.length} variant="primary" onClick={onStepChange}>
<span className="flex gap-0.5 px-[10px]">
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
<RiArrowRightLine className="size-4" />
@ -330,12 +332,12 @@ const StepOne = ({
/>
</div>
{isShowVectorSpaceFull && (
<div className='mb-4 max-w-[640px]'>
<div className="mb-4 max-w-[640px]">
<VectorSpaceFull />
</div>
)}
<div className="flex max-w-[640px] justify-end gap-2">
<Button disabled={isShowVectorSpaceFull || !websitePages.length} variant='primary' onClick={onStepChange}>
<Button disabled={isShowVectorSpaceFull || !websitePages.length} variant="primary" onClick={onStepChange}>
<span className="flex gap-0.5 px-[10px]">
<span className="px-0.5">{t('datasetCreation.stepOne.button')}</span>
<RiArrowRightLine className="size-4" />
@ -346,7 +348,7 @@ const StepOne = ({
)}
{!datasetId && (
<>
<div className='my-8 h-px max-w-[640px] bg-divider-regular' />
<div className="my-8 h-px max-w-[640px] bg-divider-regular" />
<span className="inline-flex cursor-pointer items-center text-[13px] leading-4 text-text-accent" onClick={modalShowHandle}>
<RiFolder6Line className="mr-1 size-4" />
{t('datasetCreation.stepOne.emptyDatasetCreation')}
@ -357,7 +359,7 @@ const StepOne = ({
<EmptyDatasetCreationModal show={showModal} onHide={modalCloseHandle} />
</div>
</div>
<div className='h-full w-1/2 overflow-y-auto'>
<div className="h-full w-1/2 overflow-y-auto">
{currentFile && <FilePreview file={currentFile} hidePreview={hideFilePreview} />}
{currentNotionPage && (
<NotionPagePreview

View File

@ -1,9 +1,9 @@
'use client'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import { useModalContext } from '@/context/modal-context'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import { useModalContext } from '@/context/modal-context'
const UpgradeCard: FC = () => {
const { t } = useTranslation()
@ -14,17 +14,17 @@ const UpgradeCard: FC = () => {
}, [setShowPricingModal])
return (
<div className='flex items-center justify-between rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg py-3 pl-4 pr-3.5 shadow-xs backdrop-blur-[5px] '>
<div className="flex items-center justify-between rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg py-3 pl-4 pr-3.5 shadow-xs backdrop-blur-[5px] ">
<div>
<div className='title-md-semi-bold bg-[linear-gradient(92deg,_var(--components-input-border-active-prompt-1,_#0BA5EC)_0%,_var(--components-input-border-active-prompt-2,_#155AEF)_99.21%)] bg-clip-text text-transparent'>{t('billing.upgrade.uploadMultipleFiles.title')}</div>
<div className='system-xs-regular text-text-tertiary'>{t('billing.upgrade.uploadMultipleFiles.description')}</div>
<div className="title-md-semi-bold bg-[linear-gradient(92deg,_var(--components-input-border-active-prompt-1,_#0BA5EC)_0%,_var(--components-input-border-active-prompt-2,_#155AEF)_99.21%)] bg-clip-text text-transparent">{t('billing.upgrade.uploadMultipleFiles.title')}</div>
<div className="system-xs-regular text-text-tertiary">{t('billing.upgrade.uploadMultipleFiles.description')}</div>
</div>
<UpgradeBtn
size='custom'
size="custom"
isShort
className='ml-3 !h-8 !rounded-lg px-2'
labelKey='billing.triggerLimitModal.upgrade'
loc='upload-multiple-files'
className="ml-3 !h-8 !rounded-lg px-2"
labelKey="billing.triggerLimitModal.upgrade"
loc="upload-multiple-files"
onClick={handleUpgrade}
/>
</div>

View File

@ -1,6 +1,6 @@
import type { createDocumentResponse, FullDocumentDetail, IconInfo } from '@/models/datasets'
import { render, screen } from '@testing-library/react'
import StepThree from './index'
import type { FullDocumentDetail, IconInfo, createDocumentResponse } from '@/models/datasets'
// Mock the EmbeddingProcess component since it has complex async logic
vi.mock('../embedding-process', () => ({

View File

@ -1,14 +1,14 @@
'use client'
import type { createDocumentResponse, FullDocumentDetail } from '@/models/datasets'
import { RiBookOpenLine } from '@remixicon/react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { RiBookOpenLine } from '@remixicon/react'
import EmbeddingProcess from '../embedding-process'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import type { FullDocumentDetail, createDocumentResponse } from '@/models/datasets'
import AppIcon from '@/app/components/base/app-icon'
import Divider from '@/app/components/base/divider'
import { useDocLink } from '@/context/i18n'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import EmbeddingProcess from '../embedding-process'
type StepThreeProps = {
datasetId?: string
@ -32,40 +32,40 @@ const StepThree = ({ datasetId, datasetName, indexingType, creationCache, retrie
}
return (
<div className='flex h-full max-h-full w-full justify-center overflow-y-auto'>
<div className='h-full max-w-[960px] shrink-0 grow overflow-y-auto px-14 sm:px-16'>
<div className='mx-auto max-w-[640px] pb-8 pt-10'>
<div className="flex h-full max-h-full w-full justify-center overflow-y-auto">
<div className="h-full max-w-[960px] shrink-0 grow overflow-y-auto px-14 sm:px-16">
<div className="mx-auto max-w-[640px] pb-8 pt-10">
{!datasetId && (
<>
<div className='flex flex-col gap-y-1 pb-3'>
<div className='title-2xl-semi-bold text-text-primary'>{t('datasetCreation.stepThree.creationTitle')}</div>
<div className='system-sm-regular text-text-tertiary'>{t('datasetCreation.stepThree.creationContent')}</div>
<div className="flex flex-col gap-y-1 pb-3">
<div className="title-2xl-semi-bold text-text-primary">{t('datasetCreation.stepThree.creationTitle')}</div>
<div className="system-sm-regular text-text-tertiary">{t('datasetCreation.stepThree.creationContent')}</div>
</div>
<div className='flex items-center gap-x-4'>
<div className="flex items-center gap-x-4">
<AppIcon
size='xxl'
size="xxl"
iconType={iconInfo.icon_type}
icon={iconInfo.icon}
background={iconInfo.icon_background}
imageUrl={iconInfo.icon_url}
className='shrink-0'
className="shrink-0"
/>
<div className='flex grow flex-col gap-y-1'>
<div className='system-sm-semibold flex h-6 items-center text-text-secondary'>
<div className="flex grow flex-col gap-y-1">
<div className="system-sm-semibold flex h-6 items-center text-text-secondary">
{t('datasetCreation.stepThree.label')}
</div>
<div className='system-sm-regular w-full truncate rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled'>
<span className='px-1'>{datasetName || creationCache?.dataset?.name}</span>
<div className="system-sm-regular w-full truncate rounded-lg bg-components-input-bg-normal p-2 text-components-input-text-filled">
<span className="px-1">{datasetName || creationCache?.dataset?.name}</span>
</div>
</div>
</div>
<Divider type='horizontal' className='my-6 bg-divider-subtle' />
<Divider type="horizontal" className="my-6 bg-divider-subtle" />
</>
)}
{datasetId && (
<div className='flex flex-col gap-y-1 pb-3'>
<div className='title-2xl-semi-bold text-text-primary'>{t('datasetCreation.stepThree.additionTitle')}</div>
<div className='system-sm-regular text-text-tertiary'>{`${t('datasetCreation.stepThree.additionP1')} ${datasetName || creationCache?.dataset?.name} ${t('datasetCreation.stepThree.additionP2')}`}</div>
<div className="flex flex-col gap-y-1 pb-3">
<div className="title-2xl-semi-bold text-text-primary">{t('datasetCreation.stepThree.additionTitle')}</div>
<div className="system-sm-regular text-text-tertiary">{`${t('datasetCreation.stepThree.additionP1')} ${datasetName || creationCache?.dataset?.name} ${t('datasetCreation.stepThree.additionP2')}`}</div>
</div>
)}
<EmbeddingProcess
@ -78,18 +78,18 @@ const StepThree = ({ datasetId, datasetName, indexingType, creationCache, retrie
</div>
</div>
{!isMobile && (
<div className='shrink-0 pr-8 pt-[88px] text-xs'>
<div className='flex w-[328px] flex-col gap-3 rounded-xl bg-background-section p-6 text-text-tertiary'>
<div className='flex size-10 items-center justify-center rounded-[10px] bg-components-card-bg shadow-lg'>
<RiBookOpenLine className='size-5 text-text-accent' />
<div className="shrink-0 pr-8 pt-[88px] text-xs">
<div className="flex w-[328px] flex-col gap-3 rounded-xl bg-background-section p-6 text-text-tertiary">
<div className="flex size-10 items-center justify-center rounded-[10px] bg-components-card-bg shadow-lg">
<RiBookOpenLine className="size-5 text-text-accent" />
</div>
<div className='text-base font-semibold text-text-secondary'>{t('datasetCreation.stepThree.sideTipTitle')}</div>
<div className='text-text-tertiary'>{t('datasetCreation.stepThree.sideTipContent')}</div>
<div className="text-base font-semibold text-text-secondary">{t('datasetCreation.stepThree.sideTipTitle')}</div>
<div className="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>

File diff suppressed because it is too large Load Diff

View File

@ -1,79 +1,96 @@
import type { FC, PropsWithChildren, ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import type { InputProps } from '@/app/components/base/input'
import Input from '@/app/components/base/input'
import Tooltip from '@/app/components/base/tooltip'
import type { InputNumberProps } from '@/app/components/base/input-number'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { InputNumber } from '@/app/components/base/input-number'
import Tooltip from '@/app/components/base/tooltip'
const TextLabel: FC<PropsWithChildren> = (props) => {
return <label className='text-xs font-semibold leading-none text-text-secondary'>{props.children}</label>
return <label className="text-xs font-semibold leading-none text-text-secondary">{props.children}</label>
}
const FormField: FC<PropsWithChildren<{ label: ReactNode }>> = (props) => {
return <div className='flex-1 space-y-2'>
<TextLabel>{props.label}</TextLabel>
{props.children}
</div>
return (
<div className="flex-1 space-y-2">
<TextLabel>{props.label}</TextLabel>
{props.children}
</div>
)
}
export const DelimiterInput: FC<InputProps & { tooltip?: string }> = (props) => {
const { t } = useTranslation()
return <FormField label={<div className='mb-1 flex items-center'>
<span className='system-sm-semibold mr-0.5'>{t('datasetCreation.stepTwo.separator')}</span>
<Tooltip
popupContent={
<div className='max-w-[200px]'>
{props.tooltip || t('datasetCreation.stepTwo.separatorTip')}
</div>
}
/>
</div>}>
<Input
type="text"
className='h-9'
placeholder={t('datasetCreation.stepTwo.separatorPlaceholder')!}
{...props}
/>
</FormField>
return (
<FormField label={(
<div className="mb-1 flex items-center">
<span className="system-sm-semibold mr-0.5">{t('datasetCreation.stepTwo.separator')}</span>
<Tooltip
popupContent={(
<div className="max-w-[200px]">
{props.tooltip || t('datasetCreation.stepTwo.separatorTip')}
</div>
)}
/>
</div>
)}
>
<Input
type="text"
className="h-9"
placeholder={t('datasetCreation.stepTwo.separatorPlaceholder')!}
{...props}
/>
</FormField>
)
}
export const MaxLengthInput: FC<InputNumberProps> = (props) => {
const maxValue = Number.parseInt(globalThis.document?.body?.getAttribute('data-public-indexing-max-segmentation-tokens-length') || '4000', 10)
const { t } = useTranslation()
return <FormField label={<div className='system-sm-semibold mb-1'>
{t('datasetCreation.stepTwo.maxLength')}
</div>}>
<InputNumber
type="number"
size='large'
placeholder={`${maxValue}`}
max={maxValue}
min={1}
{...props}
/>
</FormField>
return (
<FormField label={(
<div className="system-sm-semibold mb-1">
{t('datasetCreation.stepTwo.maxLength')}
</div>
)}
>
<InputNumber
type="number"
size="large"
placeholder={`${maxValue}`}
max={maxValue}
min={1}
{...props}
/>
</FormField>
)
}
export const OverlapInput: FC<InputNumberProps> = (props) => {
const { t } = useTranslation()
return <FormField label={<div className='mb-1 flex items-center'>
<span className='system-sm-semibold'>{t('datasetCreation.stepTwo.overlap')}</span>
<Tooltip
popupContent={
<div className='max-w-[200px]'>
{t('datasetCreation.stepTwo.overlapTip')}
</div>
}
/>
</div>}>
<InputNumber
type="number"
size='large'
placeholder={t('datasetCreation.stepTwo.overlap') || ''}
min={1}
{...props}
/>
</FormField>
return (
<FormField label={(
<div className="mb-1 flex items-center">
<span className="system-sm-semibold">{t('datasetCreation.stepTwo.overlap')}</span>
<Tooltip
popupContent={(
<div className="max-w-[200px]">
{t('datasetCreation.stepTwo.overlapTip')}
</div>
)}
/>
</div>
)}
>
<InputNumber
type="number"
size="large"
placeholder={t('datasetCreation.stepTwo.overlap') || ''}
min={1}
{...props}
/>
</FormField>
)
}

View File

@ -1,8 +1,8 @@
import type { ILanguageSelectProps } from './index'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import LanguageSelect from './index'
import type { ILanguageSelectProps } from './index'
import { languages } from '@/i18n-config/language'
import LanguageSelect from './index'
// Get supported languages for test assertions
const supportedLanguages = languages.filter(lang => lang.supported)

View File

@ -1,10 +1,10 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
import { cn } from '@/utils/classnames'
import React from 'react'
import Popover from '@/app/components/base/popover'
import { languages } from '@/i18n-config/language'
import { cn } from '@/utils/classnames'
export type ILanguageSelectProps = {
currentLanguage: string
@ -20,42 +20,44 @@ const LanguageSelect: FC<ILanguageSelectProps> = ({
return (
<Popover
manualClose
trigger='click'
trigger="click"
disabled={disabled}
popupClassName='z-20'
htmlContent={
<div className='w-full p-1'>
popupClassName="z-20"
htmlContent={(
<div className="w-full p-1">
{languages.filter(language => language.supported).map(({ prompt_name }) => (
<div
key={prompt_name}
className='inline-flex w-full cursor-pointer items-center justify-between rounded-lg px-3 py-2 hover:bg-state-base-hover'
className="inline-flex w-full cursor-pointer items-center justify-between rounded-lg px-3 py-2 hover:bg-state-base-hover"
onClick={() => onSelect(prompt_name)}
>
<span className='system-sm-medium text-text-secondary'>{prompt_name}</span>
{(currentLanguage === prompt_name) && <RiCheckLine className='size-4 text-text-accent' />}
<span className="system-sm-medium text-text-secondary">{prompt_name}</span>
{(currentLanguage === prompt_name) && <RiCheckLine className="size-4 text-text-accent" />}
</div>
))}
</div>
}
btnElement={
)}
btnElement={(
<div className={cn('inline-flex items-center gap-x-[1px]', disabled && 'cursor-not-allowed')}>
<span className={cn(
'system-xs-semibold px-[3px] text-components-button-tertiary-text',
disabled ? 'text-components-button-tertiary-text-disabled' : '',
)}>
)}
>
{currentLanguage}
</span>
<RiArrowDownSLine className={cn(
'size-3.5 text-components-button-tertiary-text',
disabled ? 'text-components-button-tertiary-text-disabled' : '',
)} />
)}
/>
</div>
}
)}
btnClassName={() => cn(
'!hover:bg-components-button-tertiary-bg !mx-1 rounded-md !border-0 !bg-components-button-tertiary-bg !px-1.5 !py-1',
disabled ? 'bg-components-button-tertiary-bg-disabled' : '',
)}
className='!left-1 !z-20 h-fit !w-[140px] !translate-x-0'
className="!left-1 !z-20 h-fit !w-[140px] !translate-x-0"
/>
)
}

View File

@ -20,25 +20,25 @@ type OptionCardHeaderProps = {
export const OptionCardHeader: FC<OptionCardHeaderProps> = (props) => {
const { icon, title, description, isActive, activeClassName, effectImg, disabled } = props
return <div className={cn('relative flex h-full overflow-hidden rounded-t-xl',
isActive && activeClassName,
!disabled && 'cursor-pointer')}>
<div className='relative flex size-14 items-center justify-center overflow-hidden'>
{isActive && effectImg && <Image src={effectImg} className='absolute left-0 top-0 h-full w-full' alt='' width={56} height={56} />}
<div className='p-1'>
<div className='flex size-8 justify-center rounded-lg border border-components-panel-border-subtle bg-background-default-dodge p-1.5 shadow-md'>
{icon}
return (
<div className={cn('relative flex h-full overflow-hidden rounded-t-xl', isActive && activeClassName, !disabled && 'cursor-pointer')}>
<div className="relative flex size-14 items-center justify-center overflow-hidden">
{isActive && effectImg && <Image src={effectImg} className="absolute left-0 top-0 h-full w-full" alt="" width={56} height={56} />}
<div className="p-1">
<div className="flex size-8 justify-center rounded-lg border border-components-panel-border-subtle bg-background-default-dodge p-1.5 shadow-md">
{icon}
</div>
</div>
</div>
<TriangleArrow
className={cn('absolute -bottom-1.5 left-4 text-transparent', isActive && 'text-components-panel-bg')}
/>
<div className="flex-1 space-y-0.5 py-3 pr-4">
<div className="system-md-semibold text-text-secondary">{title}</div>
<div className="system-xs-regular text-text-tertiary">{description}</div>
</div>
</div>
<TriangleArrow
className={cn('absolute -bottom-1.5 left-4 text-transparent', isActive && 'text-components-panel-bg')}
/>
<div className='flex-1 space-y-0.5 py-3 pr-4'>
<div className='system-md-semibold text-text-secondary'>{title}</div>
<div className='system-xs-regular text-text-tertiary'>{description}</div>
</div>
</div>
)
}
type OptionCardProps = {
@ -64,12 +64,9 @@ export const OptionCard: FC<OptionCardProps> = (
const { icon, className, title, description, isActive, children, actions, activeHeaderClassName, style, effectImg, onSwitched, noHighlight, disabled, ...rest } = props
return (
<div
className={cn('rounded-xl bg-components-option-card-option-bg shadow-xs',
(isActive && !noHighlight)
? 'border-[1.5px] border-components-option-card-option-selected-border'
: 'border border-components-option-card-option-border',
disabled && 'pointer-events-none opacity-50',
className)}
className={cn('rounded-xl bg-components-option-card-option-bg shadow-xs', (isActive && !noHighlight)
? 'border-[1.5px] border-components-option-card-option-selected-border'
: 'border border-components-option-card-option-border', disabled && 'pointer-events-none opacity-50', className)}
style={{
...style,
}}
@ -90,13 +87,16 @@ export const OptionCard: FC<OptionCardProps> = (
disabled={disabled}
/>
{/** Body */}
{isActive && (children || actions) && <div className='rounded-b-xl bg-components-panel-bg px-4 py-3'>
{children}
{actions && <div className='mt-4 flex gap-2'>
{actions}
{isActive && (children || actions) && (
<div className="rounded-b-xl bg-components-panel-bg px-4 py-3">
{children}
{actions && (
<div className="mt-4 flex gap-2">
{actions}
</div>
)}
</div>
}
</div>}
)}
</div>
)
}

View File

@ -1,7 +1,7 @@
import type { IPreviewItemProps } from './index'
import { render, screen } from '@testing-library/react'
import React from 'react'
import PreviewItem, { PreviewType } from './index'
import type { IPreviewItemProps } from './index'
// Test data builder for props
const createDefaultProps = (overrides?: Partial<IPreviewItemProps>): IPreviewItemProps => ({

View File

@ -44,29 +44,33 @@ const PreviewItem: FC<IPreviewItemProps> = ({
const formattedIndex = (() => String(index).padStart(3, '0'))()
return (
<div className='rounded-xl bg-gray-50 p-4'>
<div className='flex h-5 items-center justify-between text-xs text-gray-500'>
<div className='box-border flex h-[18px] items-center space-x-1 rounded-md border border-gray-200 pl-1 pr-1.5 font-medium italic'>
<div className="rounded-xl bg-gray-50 p-4">
<div className="flex h-5 items-center justify-between text-xs text-gray-500">
<div className="box-border flex h-[18px] items-center space-x-1 rounded-md border border-gray-200 pl-1 pr-1.5 font-medium italic">
{sharpIcon}
<span>{formattedIndex}</span>
</div>
<div className='flex items-center space-x-1'>
<div className="flex items-center space-x-1">
{textIcon}
<span>{charNums} {t('datasetCreation.stepTwo.characters')}</span>
<span>
{charNums}
{' '}
{t('datasetCreation.stepTwo.characters')}
</span>
</div>
</div>
<div className='mt-2 line-clamp-6 max-h-[120px] overflow-hidden text-sm text-gray-800'>
<div className="mt-2 line-clamp-6 max-h-[120px] overflow-hidden text-sm text-gray-800">
{type === PreviewType.TEXT && (
<div style={{ whiteSpace: 'pre-line' }}>{content}</div>
)}
{type === PreviewType.QA && (
<div style={{ whiteSpace: 'pre-line' }}>
<div className='flex'>
<div className='text-medium mr-2 shrink-0 text-gray-400'>Q</div>
<div className="flex">
<div className="text-medium mr-2 shrink-0 text-gray-400">Q</div>
<div style={{ whiteSpace: 'pre-line' }}>{qa?.question}</div>
</div>
<div className='flex'>
<div className='text-medium mr-2 shrink-0 text-gray-400'>A</div>
<div className="flex">
<div className="text-medium mr-2 shrink-0 text-gray-400">A</div>
<div style={{ whiteSpace: 'pre-line' }}>{qa?.answer}</div>
</div>
</div>

View File

@ -1,6 +1,8 @@
import type { StepperProps } from './index'
import type { Step, StepperStepProps } from './step'
import { render, screen } from '@testing-library/react'
import { Stepper, type StepperProps } from './index'
import { type Step, StepperStep, type StepperStepProps } from './step'
import { Stepper } from './index'
import { StepperStep } from './step'
// Test data factory for creating steps
const createStep = (overrides: Partial<Step> = {}): Step => ({

View File

@ -1,5 +1,6 @@
import { type FC, Fragment } from 'react'
import type { FC } from 'react'
import type { Step } from './step'
import { Fragment } from 'react'
import { StepperStep } from './step'
export type StepperProps = {
@ -9,19 +10,21 @@ export type StepperProps = {
export const Stepper: FC<StepperProps> = (props) => {
const { steps, activeIndex } = props
return <div className='flex items-center gap-3'>
{steps.map((step, index) => {
const isLast = index === steps.length - 1
return (
<Fragment key={index}>
<StepperStep
{...step}
activeIndex={activeIndex}
index={index}
/>
{!isLast && <div className='h-px w-4 bg-divider-deep' />}
</Fragment>
)
})}
</div>
return (
<div className="flex items-center gap-3">
{steps.map((step, index) => {
const isLast = index === steps.length - 1
return (
<Fragment key={index}>
<StepperStep
{...step}
activeIndex={activeIndex}
index={index}
/>
{!isLast && <div className="h-px w-4 bg-divider-deep" />}
</Fragment>
)
})}
</div>
)
}

View File

@ -15,27 +15,31 @@ export const StepperStep: FC<StepperStepProps> = (props) => {
const isActive = index === activeIndex
const isDisabled = activeIndex < index
const label = isActive ? `STEP ${index + 1}` : `${index + 1}`
return <div className='flex items-center gap-2'>
<div className={cn('inline-flex h-5 flex-col items-center justify-center gap-2 rounded-3xl py-1',
isActive
return (
<div className="flex items-center gap-2">
<div className={cn('inline-flex h-5 flex-col items-center justify-center gap-2 rounded-3xl py-1', isActive
? 'bg-state-accent-solid px-2'
: !isDisabled
? 'w-5 border border-text-quaternary'
: 'w-5 border border-divider-deep')}>
<div className={cn('system-2xs-semibold-uppercase text-center',
isActive
? 'w-5 border border-text-quaternary'
: 'w-5 border border-divider-deep')}
>
<div className={cn('system-2xs-semibold-uppercase text-center', isActive
? 'text-text-primary-on-surface'
: !isDisabled
? 'text-text-tertiary'
: 'text-text-quaternary')}>
{label}
? 'text-text-tertiary'
: 'text-text-quaternary')}
>
{label}
</div>
</div>
</div>
<div className={cn('system-xs-medium-uppercase',
isActive
<div className={cn('system-xs-medium-uppercase', isActive
? 'system-xs-semibold-uppercase text-text-accent'
: !isDisabled
? 'text-text-tertiary'
: 'text-text-quaternary')}>{name}</div>
</div>
? 'text-text-tertiary'
: 'text-text-quaternary')}
>
{name}
</div>
</div>
)
}

View File

@ -1,10 +1,10 @@
'use client'
import React from 'react'
import { useTranslation } from 'react-i18next'
import s from './index.module.css'
import { cn } from '@/utils/classnames'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Modal from '@/app/components/base/modal'
import { cn } from '@/utils/classnames'
import s from './index.module.css'
type IProps = {
show: boolean
@ -34,9 +34,9 @@ const StopEmbeddingModal = ({
<span className={s.close} onClick={onHide} />
<div className={s.title}>{t('datasetCreation.stepThree.modelTitle')}</div>
<div className={s.content}>{t('datasetCreation.stepThree.modelContent')}</div>
<div className='flex flex-row-reverse'>
<Button className='ml-2 w-24' variant='primary' onClick={submit}>{t('datasetCreation.stepThree.modelButtonConfirm')}</Button>
<Button className='w-24' onClick={onHide}>{t('datasetCreation.stepThree.modelButtonCancel')}</Button>
<div className="flex flex-row-reverse">
<Button className="ml-2 w-24" variant="primary" onClick={submit}>{t('datasetCreation.stepThree.modelButtonConfirm')}</Button>
<Button className="w-24" onClick={onHide}>{t('datasetCreation.stepThree.modelButtonCancel')}</Button>
</div>
</Modal>
)

View File

@ -1,9 +1,10 @@
import type { TopBarProps } from './index'
import { render, screen } from '@testing-library/react'
import { TopBar, type TopBarProps } from './index'
import { TopBar } from './index'
// Mock next/link to capture href values
vi.mock('next/link', () => ({
default: ({ children, href, replace, className }: { children: React.ReactNode; href: string; replace?: boolean; className?: string }) => (
default: ({ children, href, replace, className }: { children: React.ReactNode, href: string, replace?: boolean, className?: string }) => (
<a href={href} data-replace={replace} className={className} data-testid="back-link">
{children}
</a>

View File

@ -1,9 +1,11 @@
import { type FC, useMemo } from 'react'
import type { FC } from 'react'
import type { StepperProps } from '../stepper'
import { RiArrowLeftLine } from '@remixicon/react'
import Link from 'next/link'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Stepper, type StepperProps } from '../stepper'
import { cn } from '@/utils/classnames'
import { Stepper } from '../stepper'
export type TopBarProps = Pick<StepperProps, 'activeIndex'> & {
className?: string
@ -24,24 +26,24 @@ export const TopBar: FC<TopBarProps> = (props) => {
return datasetId ? `/datasets/${datasetId}/documents` : '/datasets'
}, [datasetId])
return <div className={cn('relative flex h-[52px] shrink-0 items-center justify-between border-b border-b-divider-subtle', className)}>
<Link href={fallbackRoute} replace className="inline-flex h-12 items-center justify-start gap-1 py-2 pl-2 pr-6">
<div className='p-2'>
<RiArrowLeftLine className='size-4 text-text-primary' />
return (
<div className={cn('relative flex h-[52px] shrink-0 items-center justify-between border-b border-b-divider-subtle', className)}>
<Link href={fallbackRoute} replace className="inline-flex h-12 items-center justify-start gap-1 py-2 pl-2 pr-6">
<div className="p-2">
<RiArrowLeftLine className="size-4 text-text-primary" />
</div>
<p className="system-sm-semibold-uppercase text-text-primary">
{t('datasetCreation.steps.header.fallbackRoute')}
</p>
</Link>
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
<Stepper
steps={Array.from({ length: 3 }, (_, i) => ({
name: t(STEP_T_MAP[i + 1]),
}))}
{...rest}
/>
</div>
<p className="system-sm-semibold-uppercase text-text-primary">
{t('datasetCreation.steps.header.fallbackRoute')}
</p>
</Link>
<div className={
'absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2'
}>
<Stepper
steps={Array.from({ length: 3 }, (_, i) => ({
name: t(STEP_T_MAP[i + 1]),
}))}
{...rest}
/>
</div>
</div>
)
}

View File

@ -1,10 +1,10 @@
import type { CrawlResultItem } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Input from './base/input'
import Header from './base/header'
import CrawledResult from './base/crawled-result'
import CrawledResultItem from './base/crawled-result-item'
import type { CrawlResultItem } from '@/models/datasets'
import Header from './base/header'
import Input from './base/input'
// ============================================================================
// Test Data Factories

View File

@ -1,9 +1,9 @@
'use client'
import type { FC } from 'react'
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 Props = {
className?: string
@ -31,9 +31,9 @@ const CheckboxWithLabel: FC<Props> = ({
{tooltip && (
<Tooltip
popupContent={
<div className='w-[200px]'>{tooltip}</div>
<div className="w-[200px]">{tooltip}</div>
}
triggerClassName='ml-0.5 w-4 h-4'
triggerClassName="ml-0.5 w-4 h-4"
/>
)}
</label>

View File

@ -1,11 +1,11 @@
'use client'
import type { FC } from 'react'
import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
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 Checkbox from '@/app/components/base/checkbox'
import { cn } from '@/utils/classnames'
type Props = {
payload: CrawlResultItemType
@ -31,19 +31,19 @@ const CrawledResultItem: FC<Props> = ({
}, [isChecked, onCheckChange])
return (
<div className={cn(isPreview ? 'bg-state-base-active' : 'group hover:bg-state-base-hover', 'cursor-pointer rounded-lg p-2')}>
<div className='relative flex'>
<div className='flex h-5 items-center'>
<Checkbox className='mr-2 shrink-0' checked={isChecked} onCheck={handleCheckChange} id={testId} />
<div className="relative flex">
<div className="flex h-5 items-center">
<Checkbox className="mr-2 shrink-0" checked={isChecked} onCheck={handleCheckChange} id={testId} />
</div>
<div className='flex min-w-0 grow flex-col'>
<div className="flex min-w-0 grow flex-col">
<div
className='truncate text-sm font-medium text-text-secondary'
className="truncate text-sm font-medium text-text-secondary"
title={payload.title}
>
{payload.title}
</div>
<div
className='mt-0.5 truncate text-xs text-text-tertiary'
className="mt-0.5 truncate text-xs text-text-tertiary"
title={payload.source_url}
>
{payload.source_url}
@ -51,7 +51,7 @@ const CrawledResultItem: FC<Props> = ({
</div>
<Button
onClick={onPreview}
className='right-0 top-0 hidden h-6 px-1.5 text-xs font-medium uppercase group-hover:absolute group-hover:block'
className="right-0 top-0 hidden h-6 px-1.5 text-xs font-medium uppercase group-hover:absolute group-hover:block"
>
{t('datasetCreation.stepOne.website.preview')}
</Button>

View File

@ -1,11 +1,11 @@
'use client'
import type { FC } from 'react'
import type { CrawlResultItem } from '@/models/datasets'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
import CheckboxWithLabel from './checkbox-with-label'
import CrawledResultItem from './crawled-result-item'
import { cn } from '@/utils/classnames'
import type { CrawlResultItem } from '@/models/datasets'
const I18N_PREFIX = 'datasetCreation.stepOne.website'
@ -58,22 +58,22 @@ const CrawledResult: FC<Props> = ({
return (
<div className={cn(className, 'border-t-[0.5px] border-divider-regular shadow-xs shadow-shadow-shadow-3')}>
<div className='flex h-[34px] items-center justify-between px-4'>
<div className="flex h-[34px] items-center justify-between px-4">
<CheckboxWithLabel
isChecked={isCheckAll}
onChange={handleCheckedAll}
label={isCheckAll ? t(`${I18N_PREFIX}.resetAll`) : t(`${I18N_PREFIX}.selectAll`)}
labelClassName='system-[13px] leading-[16px] font-medium text-text-secondary'
testId='select-all'
labelClassName="system-[13px] leading-[16px] font-medium text-text-secondary"
testId="select-all"
/>
<div className='text-xs text-text-tertiary'>
<div className="text-xs text-text-tertiary">
{t(`${I18N_PREFIX}.scrapTimeInfo`, {
total: list.length,
time: usedTime.toFixed(1),
})}
</div>
</div>
<div className='p-2'>
<div className="p-2">
{list.map((item, index) => (
<CrawledResultItem
key={item.source_url}

View File

@ -19,15 +19,20 @@ const Crawling: FC<Props> = ({
return (
<div className={className}>
<div className='flex h-[34px] items-center border-y-[0.5px] border-divider-regular px-4
text-xs text-text-tertiary shadow-xs shadow-shadow-shadow-3'>
{t('datasetCreation.stepOne.website.totalPageScraped')} {crawledNum}/{totalNum}
<div className="flex h-[34px] items-center border-y-[0.5px] border-divider-regular px-4
text-xs text-text-tertiary shadow-xs shadow-shadow-shadow-3"
>
{t('datasetCreation.stepOne.website.totalPageScraped')}
{' '}
{crawledNum}
/
{totalNum}
</div>
<div className='p-2'>
<div className="p-2">
{['', '', '', ''].map((item, index) => (
<div className='py-[5px]' key={index}>
<RowStruct className='text-text-quaternary' />
<div className="py-[5px]" key={index}>
<RowStruct className="text-text-quaternary" />
</div>
))}
</div>

View File

@ -1,8 +1,8 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { cn } from '@/utils/classnames'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import { cn } from '@/utils/classnames'
type Props = {
className?: string
@ -17,12 +17,12 @@ const ErrorMessage: FC<Props> = ({
}) => {
return (
<div className={cn(className, 'border-t border-divider-subtle bg-dataset-warning-message-bg px-4 py-2 opacity-40')}>
<div className='flex h-5 items-center'>
<AlertTriangle className='mr-2 h-4 w-4 text-text-warning-secondary' />
<div className='system-md-medium text-text-warning'>{title}</div>
<div className="flex h-5 items-center">
<AlertTriangle className="mr-2 h-4 w-4 text-text-warning-secondary" />
<div className="system-md-medium text-text-warning">{title}</div>
</div>
{errorMsg && (
<div className='system-xs-regular mt-1 pl-6 text-text-secondary'>{errorMsg}</div>
<div className="system-xs-regular mt-1 pl-6 text-text-secondary">{errorMsg}</div>
)}
</div>
)

View File

@ -1,9 +1,9 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import Input from './input'
import { cn } from '@/utils/classnames'
import Tooltip from '@/app/components/base/tooltip'
import { cn } from '@/utils/classnames'
import Input from './input'
type Props = {
className?: string
@ -30,15 +30,18 @@ const Field: FC<Props> = ({
}) => {
return (
<div className={cn(className)}>
<div className='flex py-[7px]'>
<div className={cn(labelClassName, 'flex h-[16px] items-center text-[13px] font-semibold text-text-secondary')}>{label} </div>
{isRequired && <span className='ml-0.5 text-xs font-semibold text-text-destructive'>*</span>}
<div className="flex py-[7px]">
<div className={cn(labelClassName, 'flex h-[16px] items-center text-[13px] font-semibold text-text-secondary')}>
{label}
{' '}
</div>
{isRequired && <span className="ml-0.5 text-xs font-semibold text-text-destructive">*</span>}
{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"
/>
)}
</div>

View File

@ -1,8 +1,8 @@
import React from 'react'
import Divider from '@/app/components/base/divider'
import Button from '@/app/components/base/button'
import { cn } from '@/utils/classnames'
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 { cn } from '@/utils/classnames'
type HeaderProps = {
isInPipeline?: boolean
@ -22,37 +22,38 @@ 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="flex items-center gap-x-2">
<div className="flex shrink-0 grow items-center gap-x-1">
<div className={cn(
'text-text-secondary',
isInPipeline ? 'system-sm-semibold' : 'system-md-semibold',
)}>
)}
>
{title}
</div>
<Divider type='vertical' className='mx-1 h-3.5' />
<Divider type="vertical" className="mx-1 h-3.5" />
<Button
variant='secondary'
size='small'
variant="secondary"
size="small"
className={cn(isInPipeline ? 'size-6 px-1' : 'gap-x-0.5 px-1.5')}
onClick={onClickConfiguration}
>
<RiEqualizer2Line className='size-4' />
<RiEqualizer2Line className="size-4" />
{!isInPipeline && (
<span className='system-xs-medium'>
<span className="system-xs-medium">
{buttonText}
</span>
)}
</Button>
</div>
<a
className='system-xs-medium flex items-center gap-x-1 overflow-hidden text-text-accent'
className="system-xs-medium flex items-center gap-x-1 overflow-hidden text-text-accent"
href={docLink}
target='_blank'
rel='noopener noreferrer'
target="_blank"
rel="noopener noreferrer"
>
<RiBookOpenLine className='size-3.5 shrink-0' />
<span className='grow truncate' title={docTitle}>{docTitle}</span>
<RiBookOpenLine className="size-3.5 shrink-0" />
<span className="grow truncate" title={docTitle}>{docTitle}</span>
</a>
</div>
)

View File

@ -50,12 +50,12 @@ const Input: FC<Props> = ({
{...otherOption}
value={value}
onChange={handleChange}
className='system-xs-regular focus:bg-components-inout-border-active flex h-8 w-full rounded-lg border border-transparent
className="system-xs-regular focus:bg-components-inout-border-active flex h-8 w-full rounded-lg border border-transparent
bg-components-input-bg-normal p-2 text-components-input-text-filled
caret-[#295eff] placeholder:text-components-input-text-placeholder hover:border
hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border focus:border-components-input-border-active
focus:shadow-xs focus:shadow-shadow-shadow-3
focus-visible:outline-none'
focus-visible:outline-none"
placeholder={placeholder}
/>
)

View File

@ -1,11 +1,12 @@
'use client'
import { useBoolean } from 'ahooks'
import type { FC } from 'react'
import { RiEqualizer2Line } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { RiEqualizer2Line } from '@remixicon/react'
import { cn } from '@/utils/classnames'
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
import { cn } from '@/utils/classnames'
const I18N_PREFIX = 'datasetCreation.stepOne.website'
type Props = {
@ -33,17 +34,17 @@ const OptionsWrap: FC<Props> = ({
return (
<div className={cn(className, !fold ? 'mb-0' : 'mb-3')}>
<div
className='flex h-[26px] cursor-pointer select-none items-center gap-x-1 py-1'
className="flex h-[26px] cursor-pointer select-none items-center gap-x-1 py-1"
onClick={foldToggle}
>
<div className='flex grow items-center'>
<RiEqualizer2Line className='mr-1 h-4 w-4 text-text-secondary' />
<span className='text-[13px] font-semibold uppercase leading-[16px] text-text-secondary'>{t(`${I18N_PREFIX}.options`)}</span>
<div className="flex grow items-center">
<RiEqualizer2Line className="mr-1 h-4 w-4 text-text-secondary" />
<span className="text-[13px] font-semibold uppercase leading-[16px] text-text-secondary">{t(`${I18N_PREFIX}.options`)}</span>
</div>
<ChevronRight className={cn(!fold && 'rotate-90', 'h-4 w-4 shrink-0 text-text-tertiary')} />
</div>
{!fold && (
<div className='mb-4'>
<div className="mb-4">
{children}
</div>
)}

View File

@ -2,9 +2,9 @@
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from './input'
import Button from '@/app/components/base/button'
import { useDocLink } from '@/context/i18n'
import Input from './input'
const I18N_PREFIX = 'datasetCreation.stepOne.website'
@ -30,17 +30,17 @@ const UrlInput: FC<Props> = ({
}, [isRunning, onRun, url])
return (
<div className='flex items-center justify-between gap-x-2'>
<div className="flex items-center justify-between gap-x-2">
<Input
value={url}
onChange={handleUrlChange}
placeholder={docLink()}
/>
<Button
variant='primary'
variant="primary"
onClick={handleOnRun}
loading={isRunning}
spinnerClassName='!ml-0'
spinnerClassName="!ml-0"
>
{!isRunning ? t(`${I18N_PREFIX}.run`) : ''}
</Button>

View File

@ -1,20 +1,20 @@
'use client'
import type { FC } from 'react'
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import UrlInput from '../base/url-input'
import OptionsWrap from '../base/options-wrap'
import Toast from '@/app/components/base/toast'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useModalContextSelector } from '@/context/modal-context'
import { checkFirecrawlTaskStatus, createFirecrawlTask } from '@/service/datasets'
import { sleep } from '@/utils'
import CrawledResult from '../base/crawled-result'
import Crawling from '../base/crawling'
import ErrorMessage from '../base/error-message'
import Options from './options'
import { useModalContextSelector } from '@/context/modal-context'
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import Toast from '@/app/components/base/toast'
import { checkFirecrawlTaskStatus, createFirecrawlTask } from '@/service/datasets'
import { sleep } from '@/utils'
import Header from '../base/header'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import OptionsWrap from '../base/options-wrap'
import UrlInput from '../base/url-input'
import Options from './options'
const ERROR_I18N_PREFIX = 'common.errorMsg'
const I18N_PREFIX = 'datasetCreation.stepOne.website'
@ -187,38 +187,41 @@ const FireCrawl: FC<Props> = ({
title={t(`${I18N_PREFIX}.firecrawlTitle`)}
buttonText={t(`${I18N_PREFIX}.configureFirecrawl`)}
docTitle={t(`${I18N_PREFIX}.firecrawlDoc`)}
docLink={'https://docs.firecrawl.dev/introduction'}
docLink="https://docs.firecrawl.dev/introduction"
/>
<div className='mt-2 rounded-xl border border-components-panel-border bg-background-default-subtle p-4 pb-0'>
<div className="mt-2 rounded-xl border border-components-panel-border bg-background-default-subtle p-4 pb-0">
<UrlInput onRun={handleRun} isRunning={isRunning} />
<OptionsWrap
className='mt-4'
className="mt-4"
controlFoldOptions={controlFoldOptions}
>
<Options className='mt-2' payload={crawlOptions} onChange={onCrawlOptionsChange} />
<Options className="mt-2" payload={crawlOptions} onChange={onCrawlOptionsChange} />
</OptionsWrap>
{!isInit && (
<div className='relative left-[-16px] mt-3 w-[calc(100%_+_32px)] rounded-b-xl'>
<div className="relative left-[-16px] mt-3 w-[calc(100%_+_32px)] rounded-b-xl">
{isRunning
&& <Crawling
className='mt-2'
crawledNum={crawlResult?.current || 0}
totalNum={crawlResult?.total || Number.parseFloat(crawlOptions.limit as string) || 0}
/>}
&& (
<Crawling
className="mt-2"
crawledNum={crawlResult?.current || 0}
totalNum={crawlResult?.total || Number.parseFloat(crawlOptions.limit as string) || 0}
/>
)}
{showError && (
<ErrorMessage className='rounded-b-xl' title={t(`${I18N_PREFIX}.exceptionErrorTitle`)} errorMsg={crawlErrorMessage} />
<ErrorMessage className="rounded-b-xl" title={t(`${I18N_PREFIX}.exceptionErrorTitle`)} errorMsg={crawlErrorMessage} />
)}
{isCrawlFinished && !showError
&& <CrawledResult
className='mb-2'
list={crawlResult?.data || []}
checkedList={checkedCrawlResult}
onSelectedChange={onCheckedCrawlResultChange}
onPreview={onPreview}
usedTime={Number.parseFloat(crawlResult?.time_consuming as string) || 0}
/>
}
&& (
<CrawledResult
className="mb-2"
list={crawlResult?.data || []}
checkedList={checkedCrawlResult}
onSelectedChange={onCheckedCrawlResultChange}
onPreview={onPreview}
usedTime={Number.parseFloat(crawlResult?.time_consuming as string) || 0}
/>
)}
</div>
)}
</div>

View File

@ -1,11 +1,11 @@
'use client'
import type { FC } from 'react'
import type { CrawlOptions } from '@/models/datasets'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
import CheckboxWithLabel from '../base/checkbox-with-label'
import Field from '../base/field'
import { cn } from '@/utils/classnames'
import type { CrawlOptions } from '@/models/datasets'
const I18N_PREFIX = 'datasetCreation.stepOne.website'
@ -36,11 +36,11 @@ const Options: FC<Props> = ({
label={t(`${I18N_PREFIX}.crawlSubPage`)}
isChecked={payload.crawl_sub_pages}
onChange={handleChange('crawl_sub_pages')}
labelClassName='text-[13px] leading-[16px] font-medium text-text-secondary'
labelClassName="text-[13px] leading-[16px] font-medium text-text-secondary"
/>
<div className='flex justify-between space-x-4'>
<div className="flex justify-between space-x-4">
<Field
className='shrink-0 grow'
className="shrink-0 grow"
label={t(`${I18N_PREFIX}.limit`)}
value={payload.limit}
onChange={handleChange('limit')}
@ -48,7 +48,7 @@ const Options: FC<Props> = ({
isRequired
/>
<Field
className='shrink-0 grow'
className="shrink-0 grow"
label={t(`${I18N_PREFIX}.maxDepth`)}
value={payload.max_depth}
onChange={handleChange('max_depth')}
@ -57,27 +57,27 @@ const Options: FC<Props> = ({
/>
</div>
<div className='flex justify-between space-x-4'>
<div className="flex justify-between space-x-4">
<Field
className='shrink-0 grow'
className="shrink-0 grow"
label={t(`${I18N_PREFIX}.excludePaths`)}
value={payload.excludes}
onChange={handleChange('excludes')}
placeholder='blog/*, /about/*'
placeholder="blog/*, /about/*"
/>
<Field
className='shrink-0 grow'
className="shrink-0 grow"
label={t(`${I18N_PREFIX}.includeOnlyPaths`)}
value={payload.includes}
onChange={handleChange('includes')}
placeholder='articles/*'
placeholder="articles/*"
/>
</div>
<CheckboxWithLabel
label={t(`${I18N_PREFIX}.extractOnlyMainContent`)}
isChecked={payload.only_main_content}
onChange={handleChange('only_main_content')}
labelClassName='text-[13px] leading-[16px] font-medium text-text-secondary'
labelClassName="text-[13px] leading-[16px] font-medium text-text-secondary"
/>
</div>
)

View File

@ -1,19 +1,19 @@
'use client'
import type { FC } from 'react'
import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types'
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import s from './index.module.css'
import NoData from './no-data'
import Firecrawl from './firecrawl'
import Watercrawl from './watercrawl'
import JinaReader from './jina-reader'
import { cn } from '@/utils/classnames'
import { useModalContext } from '@/context/modal-context'
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import { DataSourceProvider } from '@/models/common'
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
import { useModalContext } from '@/context/modal-context'
import { DataSourceProvider } from '@/models/common'
import { cn } from '@/utils/classnames'
import Firecrawl from './firecrawl'
import s from './index.module.css'
import JinaReader from './jina-reader'
import NoData from './no-data'
import Watercrawl from './watercrawl'
type Props = {
onPreview: (payload: CrawlResultItem) => void
@ -44,7 +44,8 @@ const Website: FC<Props> = ({
return [
DataSourceProvider.jinaReader,
DataSourceProvider.fireCrawl,
DataSourceProvider.waterCrawl].includes(item.provider as DataSourceProvider) && item.credentials_list.length > 0
DataSourceProvider.waterCrawl,
].includes(item.provider as DataSourceProvider) && item.credentials_list.length > 0
}), [authedDataSourceList])
const handleOnConfig = useCallback(() => {
@ -57,55 +58,58 @@ const Website: FC<Props> = ({
return (
<div>
<div className='mb-4'>
<div className='system-md-medium mb-2 text-text-secondary'>
<div className="mb-4">
<div className="system-md-medium mb-2 text-text-secondary">
{t('datasetCreation.stepOne.website.chooseProvider')}
</div>
<div className='flex space-x-2'>
{ENABLE_WEBSITE_JINAREADER && <button type="button"
className={cn('flex items-center justify-center rounded-lg px-4 py-2',
selectedProvider === DataSourceProvider.jinaReader
<div className="flex space-x-2">
{ENABLE_WEBSITE_JINAREADER && (
<button
type="button"
className={cn('flex items-center justify-center rounded-lg px-4 py-2', selectedProvider === DataSourceProvider.jinaReader
? 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary'
: `system-sm-regular border border-components-option-card-option-border bg-components-option-card-option-bg text-text-secondary
hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs hover:shadow-shadow-shadow-3`,
)}
onClick={() => {
setSelectedProvider(DataSourceProvider.jinaReader)
onCrawlProviderChange(DataSourceProvider.jinaReader)
}}
>
<span className={cn(s.jinaLogo, 'mr-2')} />
<span>Jina Reader</span>
</button>}
{ENABLE_WEBSITE_FIRECRAWL && <button type="button"
className={cn('rounded-lg px-4 py-2',
selectedProvider === DataSourceProvider.fireCrawl
hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs hover:shadow-shadow-shadow-3`)}
onClick={() => {
setSelectedProvider(DataSourceProvider.jinaReader)
onCrawlProviderChange(DataSourceProvider.jinaReader)
}}
>
<span className={cn(s.jinaLogo, 'mr-2')} />
<span>Jina Reader</span>
</button>
)}
{ENABLE_WEBSITE_FIRECRAWL && (
<button
type="button"
className={cn('rounded-lg px-4 py-2', selectedProvider === DataSourceProvider.fireCrawl
? 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary'
: `system-sm-regular border border-components-option-card-option-border bg-components-option-card-option-bg text-text-secondary
hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs hover:shadow-shadow-shadow-3`,
)}
onClick={() => {
setSelectedProvider(DataSourceProvider.fireCrawl)
onCrawlProviderChange(DataSourceProvider.fireCrawl)
}}
>
🔥 Firecrawl
</button>}
{ENABLE_WEBSITE_WATERCRAWL && <button type="button"
className={cn('flex items-center justify-center rounded-lg px-4 py-2',
selectedProvider === DataSourceProvider.waterCrawl
hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs hover:shadow-shadow-shadow-3`)}
onClick={() => {
setSelectedProvider(DataSourceProvider.fireCrawl)
onCrawlProviderChange(DataSourceProvider.fireCrawl)
}}
>
🔥 Firecrawl
</button>
)}
{ENABLE_WEBSITE_WATERCRAWL && (
<button
type="button"
className={cn('flex items-center justify-center rounded-lg px-4 py-2', selectedProvider === DataSourceProvider.waterCrawl
? 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary'
: `system-sm-regular border border-components-option-card-option-border bg-components-option-card-option-bg text-text-secondary
hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs hover:shadow-shadow-shadow-3`,
)}
onClick={() => {
setSelectedProvider(DataSourceProvider.waterCrawl)
onCrawlProviderChange(DataSourceProvider.waterCrawl)
}}
>
<span className={cn(s.watercrawlLogo, 'mr-2')} />
<span>WaterCrawl</span>
</button>}
hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs hover:shadow-shadow-shadow-3`)}
onClick={() => {
setSelectedProvider(DataSourceProvider.waterCrawl)
onCrawlProviderChange(DataSourceProvider.waterCrawl)
}}
>
<span className={cn(s.watercrawlLogo, 'mr-2')} />
<span>WaterCrawl</span>
</button>
)}
</div>
</div>
{source && selectedProvider === DataSourceProvider.fireCrawl && (

View File

@ -2,9 +2,9 @@
import type { FC } from 'react'
import React, { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '../../base/input'
import Button from '@/app/components/base/button'
import { useDocLink } from '@/context/i18n'
import Input from '../../base/input'
const I18N_PREFIX = 'datasetCreation.stepOne.website'
@ -30,18 +30,18 @@ const UrlInput: FC<Props> = ({
}, [isRunning, onRun, url])
return (
<div className='flex items-center justify-between'>
<div className="flex items-center justify-between">
<Input
value={url}
onChange={handleUrlChange}
placeholder={docLink()}
/>
<Button
variant='primary'
variant="primary"
onClick={handleOnRun}
className='ml-2'
className="ml-2"
loading={isRunning}
data-testid='url-input-run-button'
data-testid="url-input-run-button"
>
{!isRunning ? t(`${I18N_PREFIX}.run`) : ''}
</Button>

View File

@ -1,10 +1,10 @@
import type { Mock } from 'vitest'
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import JinaReader from './index'
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import { checkJinaReaderTaskStatus, createJinaReaderTask } from '@/service/datasets'
import { sleep } from '@/utils'
import JinaReader from './index'
// Mock external dependencies
vi.mock('@/service/datasets', () => ({
@ -748,8 +748,7 @@ describe('JinaReader', () => {
current: 100,
total: 100,
data: Array.from({ length: 100 }, (_, i) =>
createCrawlResultItem({ source_url: `https://example.com/${i}` }),
),
createCrawlResultItem({ source_url: `https://example.com/${i}` })),
})
const props = createDefaultProps({

View File

@ -1,20 +1,20 @@
'use client'
import type { FC } from 'react'
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import UrlInput from '../base/url-input'
import OptionsWrap from '../base/options-wrap'
import Toast from '@/app/components/base/toast'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useModalContext } from '@/context/modal-context'
import { checkJinaReaderTaskStatus, createJinaReaderTask } from '@/service/datasets'
import { sleep } from '@/utils'
import CrawledResult from '../base/crawled-result'
import Crawling from '../base/crawling'
import ErrorMessage from '../base/error-message'
import Options from './options'
import { useModalContext } from '@/context/modal-context'
import Toast from '@/app/components/base/toast'
import { checkJinaReaderTaskStatus, createJinaReaderTask } from '@/service/datasets'
import { sleep } from '@/utils'
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import Header from '../base/header'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import OptionsWrap from '../base/options-wrap'
import UrlInput from '../base/url-input'
import Options from './options'
const ERROR_I18N_PREFIX = 'common.errorMsg'
const I18N_PREFIX = 'datasetCreation.stepOne.website'
@ -197,38 +197,41 @@ const JinaReader: FC<Props> = ({
title={t(`${I18N_PREFIX}.jinaReaderTitle`)}
buttonText={t(`${I18N_PREFIX}.configureJinaReader`)}
docTitle={t(`${I18N_PREFIX}.jinaReaderDoc`)}
docLink={'https://jina.ai/reader'}
docLink="https://jina.ai/reader"
/>
<div className='mt-2 rounded-xl border border-components-panel-border bg-background-default-subtle p-4 pb-0'>
<div className="mt-2 rounded-xl border border-components-panel-border bg-background-default-subtle p-4 pb-0">
<UrlInput onRun={handleRun} isRunning={isRunning} />
<OptionsWrap
className='mt-4'
className="mt-4"
controlFoldOptions={controlFoldOptions}
>
<Options className='mt-2' payload={crawlOptions} onChange={onCrawlOptionsChange} />
<Options className="mt-2" payload={crawlOptions} onChange={onCrawlOptionsChange} />
</OptionsWrap>
{!isInit && (
<div className='relative left-[-16px] mt-3 w-[calc(100%_+_32px)] rounded-b-xl'>
<div className="relative left-[-16px] mt-3 w-[calc(100%_+_32px)] rounded-b-xl">
{isRunning
&& <Crawling
className='mt-2'
crawledNum={crawlResult?.current || 0}
totalNum={crawlResult?.total || Number.parseFloat(crawlOptions.limit as string) || 0}
/>}
&& (
<Crawling
className="mt-2"
crawledNum={crawlResult?.current || 0}
totalNum={crawlResult?.total || Number.parseFloat(crawlOptions.limit as string) || 0}
/>
)}
{showError && (
<ErrorMessage className='rounded-b-xl' title={t(`${I18N_PREFIX}.exceptionErrorTitle`)} errorMsg={crawlErrorMessage} />
<ErrorMessage className="rounded-b-xl" title={t(`${I18N_PREFIX}.exceptionErrorTitle`)} errorMsg={crawlErrorMessage} />
)}
{isCrawlFinished && !showError
&& <CrawledResult
className='mb-2'
list={crawlResult?.data || []}
checkedList={checkedCrawlResult}
onSelectedChange={onCheckedCrawlResultChange}
onPreview={onPreview}
usedTime={Number.parseFloat(crawlResult?.time_consuming as string) || 0}
/>
}
&& (
<CrawledResult
className="mb-2"
list={crawlResult?.data || []}
checkedList={checkedCrawlResult}
onSelectedChange={onCheckedCrawlResultChange}
onPreview={onPreview}
usedTime={Number.parseFloat(crawlResult?.time_consuming as string) || 0}
/>
)}
</div>
)}
</div>

View File

@ -1,11 +1,11 @@
'use client'
import type { FC } from 'react'
import type { CrawlOptions } from '@/models/datasets'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
import CheckboxWithLabel from '../base/checkbox-with-label'
import Field from '../base/field'
import { cn } from '@/utils/classnames'
import type { CrawlOptions } from '@/models/datasets'
const I18N_PREFIX = 'datasetCreation.stepOne.website'
@ -36,20 +36,20 @@ const Options: FC<Props> = ({
label={t(`${I18N_PREFIX}.crawlSubPage`)}
isChecked={payload.crawl_sub_pages}
onChange={handleChange('crawl_sub_pages')}
labelClassName='text-[13px] leading-[16px] font-medium text-text-secondary'
testId='crawl-sub-pages'
labelClassName="text-[13px] leading-[16px] font-medium text-text-secondary"
testId="crawl-sub-pages"
/>
<CheckboxWithLabel
label={t(`${I18N_PREFIX}.useSitemap`)}
isChecked={payload.use_sitemap}
onChange={handleChange('use_sitemap')}
tooltip={t(`${I18N_PREFIX}.useSitemapTooltip`) as string}
labelClassName='text-[13px] leading-[16px] font-medium text-text-secondary'
testId='use-sitemap'
labelClassName="text-[13px] leading-[16px] font-medium text-text-secondary"
testId="use-sitemap"
/>
<div className='flex justify-between space-x-4'>
<div className="flex justify-between space-x-4">
<Field
className='shrink-0 grow'
className="shrink-0 grow"
label={t(`${I18N_PREFIX}.limit`)}
value={payload.limit}
onChange={handleChange('limit')}

View File

@ -2,11 +2,11 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import s from './index.module.css'
import { Icon3Dots } from '@/app/components/base/icons/src/vender/line/others'
import Button from '@/app/components/base/button'
import { Icon3Dots } from '@/app/components/base/icons/src/vender/line/others'
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
import { DataSourceProvider } from '@/models/common'
import s from './index.module.css'
const I18N_PREFIX = 'datasetCreation.stepOne.website'
@ -26,44 +26,52 @@ const NoData: FC<Props> = ({
title: string
description: string
} | null> = {
[DataSourceProvider.jinaReader]: ENABLE_WEBSITE_JINAREADER ? {
emoji: <span className={s.jinaLogo} />,
title: t(`${I18N_PREFIX}.jinaReaderNotConfigured`),
description: t(`${I18N_PREFIX}.jinaReaderNotConfiguredDescription`),
} : null,
[DataSourceProvider.fireCrawl]: ENABLE_WEBSITE_FIRECRAWL ? {
emoji: '🔥',
title: t(`${I18N_PREFIX}.fireCrawlNotConfigured`),
description: t(`${I18N_PREFIX}.fireCrawlNotConfiguredDescription`),
} : null,
[DataSourceProvider.waterCrawl]: ENABLE_WEBSITE_WATERCRAWL ? {
emoji: '💧',
title: t(`${I18N_PREFIX}.waterCrawlNotConfigured`),
description: t(`${I18N_PREFIX}.waterCrawlNotConfiguredDescription`),
} : null,
[DataSourceProvider.jinaReader]: ENABLE_WEBSITE_JINAREADER
? {
emoji: <span className={s.jinaLogo} />,
title: t(`${I18N_PREFIX}.jinaReaderNotConfigured`),
description: t(`${I18N_PREFIX}.jinaReaderNotConfiguredDescription`),
}
: null,
[DataSourceProvider.fireCrawl]: ENABLE_WEBSITE_FIRECRAWL
? {
emoji: '🔥',
title: t(`${I18N_PREFIX}.fireCrawlNotConfigured`),
description: t(`${I18N_PREFIX}.fireCrawlNotConfiguredDescription`),
}
: null,
[DataSourceProvider.waterCrawl]: ENABLE_WEBSITE_WATERCRAWL
? {
emoji: '💧',
title: t(`${I18N_PREFIX}.waterCrawlNotConfigured`),
description: t(`${I18N_PREFIX}.waterCrawlNotConfiguredDescription`),
}
: null,
}
const currentProvider = providerConfig[provider] || providerConfig.jinareader
if (!currentProvider) return null
if (!currentProvider)
return null
return (
<>
<div className='mt-4 max-w-[640px] rounded-2xl bg-workflow-process-bg p-6'>
<div className='flex h-12 w-12 items-center justify-center rounded-[10px] border-[0.5px]
border-components-card-border bg-components-card-bg shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]'>
<div className="mt-4 max-w-[640px] rounded-2xl bg-workflow-process-bg p-6">
<div className="flex h-12 w-12 items-center justify-center rounded-[10px] border-[0.5px]
border-components-card-border bg-components-card-bg shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]"
>
{currentProvider.emoji}
</div>
<div className='mb-1 mt-2 flex flex-col gap-y-1 pb-3 pt-1'>
<span className='system-md-semibold text-text-secondary'>
<div className="mb-1 mt-2 flex flex-col gap-y-1 pb-3 pt-1">
<span className="system-md-semibold text-text-secondary">
{currentProvider.title}
<Icon3Dots className='relative -left-1.5 -top-2.5 inline' />
<Icon3Dots className="relative -left-1.5 -top-2.5 inline" />
</span>
<div className='system-sm-regular text-text-tertiary'>
<div className="system-sm-regular text-text-tertiary">
{currentProvider.description}
</div>
</div>
<Button variant='primary' onClick={onConfig}>
<Button variant="primary" onClick={onConfig}>
{t(`${I18N_PREFIX}.configure`)}
</Button>
</div>

View File

@ -1,10 +1,10 @@
'use client'
import type { CrawlResultItem } from '@/models/datasets'
import { XMarkIcon } from '@heroicons/react/20/solid'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { XMarkIcon } from '@heroicons/react/20/solid'
import s from '../file-preview/index.module.css'
import { cn } from '@/utils/classnames'
import type { CrawlResultItem } from '@/models/datasets'
import s from '../file-preview/index.module.css'
type IProps = {
payload: CrawlResultItem
@ -22,14 +22,14 @@ const WebsitePreview = ({
<div className={cn(s.previewHeader)}>
<div className={cn(s.title, 'title-md-semi-bold')}>
<span>{t('datasetCreation.stepOne.pagePreview')}</span>
<div className='flex h-6 w-6 cursor-pointer items-center justify-center' onClick={hidePreview}>
<XMarkIcon className='h-4 w-4'></XMarkIcon>
<div className="flex h-6 w-6 cursor-pointer items-center justify-center" onClick={hidePreview}>
<XMarkIcon className="h-4 w-4"></XMarkIcon>
</div>
</div>
<div className='title-sm-semi-bold break-words text-text-primary'>
<div className="title-sm-semi-bold break-words text-text-primary">
{payload.title}
</div>
<div className='system-xs-medium truncate text-text-tertiary' title={payload.source_url}>{payload.source_url}</div>
<div className="system-xs-medium truncate text-text-tertiary" title={payload.source_url}>{payload.source_url}</div>
</div>
<div className={cn(s.previewContent, 'body-md-regular')}>
<div className={cn(s.fileContent)}>{payload.markdown}</div>

View File

@ -1,10 +1,10 @@
import type { Mock } from 'vitest'
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import WaterCrawl from './index'
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import { checkWatercrawlTaskStatus, createWatercrawlTask } from '@/service/datasets'
import { sleep } from '@/utils'
import WaterCrawl from './index'
// Mock external dependencies
vi.mock('@/service/datasets', () => ({
@ -759,8 +759,7 @@ describe('WaterCrawl', () => {
current: 100,
total: 100,
data: Array.from({ length: 100 }, (_, i) =>
createCrawlResultItem({ source_url: `https://example.com/${i}` }),
),
createCrawlResultItem({ source_url: `https://example.com/${i}` })),
})
const props = createDefaultProps({
@ -1615,16 +1614,14 @@ describe('WaterCrawl', () => {
current: 5,
total: 10,
data: Array.from({ length: 5 }, (_, i) =>
createCrawlResultItem({ source_url: `https://page${i + 1}.com` }),
),
createCrawlResultItem({ source_url: `https://page${i + 1}.com` })),
})
.mockResolvedValueOnce({
status: 'completed',
current: 10,
total: 10,
data: Array.from({ length: 10 }, (_, i) =>
createCrawlResultItem({ source_url: `https://page${i + 1}.com` }),
),
createCrawlResultItem({ source_url: `https://page${i + 1}.com` })),
})
const props = createDefaultProps({

View File

@ -1,20 +1,20 @@
'use client'
import type { FC } from 'react'
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import UrlInput from '../base/url-input'
import OptionsWrap from '../base/options-wrap'
import Toast from '@/app/components/base/toast'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useModalContext } from '@/context/modal-context'
import { checkWatercrawlTaskStatus, createWatercrawlTask } from '@/service/datasets'
import { sleep } from '@/utils'
import CrawledResult from '../base/crawled-result'
import Crawling from '../base/crawling'
import ErrorMessage from '../base/error-message'
import Options from './options'
import { useModalContext } from '@/context/modal-context'
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import Toast from '@/app/components/base/toast'
import { checkWatercrawlTaskStatus, createWatercrawlTask } from '@/service/datasets'
import { sleep } from '@/utils'
import Header from '../base/header'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import OptionsWrap from '../base/options-wrap'
import UrlInput from '../base/url-input'
import Options from './options'
const ERROR_I18N_PREFIX = 'common.errorMsg'
const I18N_PREFIX = 'datasetCreation.stepOne.website'
@ -183,38 +183,41 @@ const WaterCrawl: FC<Props> = ({
title={t(`${I18N_PREFIX}.watercrawlTitle`)}
buttonText={t(`${I18N_PREFIX}.configureWatercrawl`)}
docTitle={t(`${I18N_PREFIX}.watercrawlDoc`)}
docLink={'https://docs.watercrawl.dev/'}
docLink="https://docs.watercrawl.dev/"
/>
<div className='mt-2 rounded-xl border border-components-panel-border bg-background-default-subtle p-4 pb-0'>
<div className="mt-2 rounded-xl border border-components-panel-border bg-background-default-subtle p-4 pb-0">
<UrlInput onRun={handleRun} isRunning={isRunning} />
<OptionsWrap
className='mt-4'
className="mt-4"
controlFoldOptions={controlFoldOptions}
>
<Options className='mt-2' payload={crawlOptions} onChange={onCrawlOptionsChange} />
<Options className="mt-2" payload={crawlOptions} onChange={onCrawlOptionsChange} />
</OptionsWrap>
{!isInit && (
<div className='relative left-[-16px] mt-3 w-[calc(100%_+_32px)] rounded-b-xl'>
<div className="relative left-[-16px] mt-3 w-[calc(100%_+_32px)] rounded-b-xl">
{isRunning
&& <Crawling
className='mt-2'
crawledNum={crawlResult?.current || 0}
totalNum={crawlResult?.total || Number.parseFloat(crawlOptions.limit as string) || 0}
/>}
&& (
<Crawling
className="mt-2"
crawledNum={crawlResult?.current || 0}
totalNum={crawlResult?.total || Number.parseFloat(crawlOptions.limit as string) || 0}
/>
)}
{showError && (
<ErrorMessage className='rounded-b-xl' title={t(`${I18N_PREFIX}.exceptionErrorTitle`)} errorMsg={crawlErrorMessage} />
<ErrorMessage className="rounded-b-xl" title={t(`${I18N_PREFIX}.exceptionErrorTitle`)} errorMsg={crawlErrorMessage} />
)}
{isCrawlFinished && !showError
&& <CrawledResult
className='mb-2'
list={crawlResult?.data || []}
checkedList={checkedCrawlResult}
onSelectedChange={onCheckedCrawlResultChange}
onPreview={onPreview}
usedTime={Number.parseFloat(crawlResult?.time_consuming as string) || 0}
/>
}
&& (
<CrawledResult
className="mb-2"
list={crawlResult?.data || []}
checkedList={checkedCrawlResult}
onSelectedChange={onCheckedCrawlResultChange}
onPreview={onPreview}
usedTime={Number.parseFloat(crawlResult?.time_consuming as string) || 0}
/>
)}
</div>
)}
</div>

View File

@ -1,11 +1,11 @@
'use client'
import type { FC } from 'react'
import type { CrawlOptions } from '@/models/datasets'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/utils/classnames'
import CheckboxWithLabel from '../base/checkbox-with-label'
import Field from '../base/field'
import { cn } from '@/utils/classnames'
import type { CrawlOptions } from '@/models/datasets'
const I18N_PREFIX = 'datasetCreation.stepOne.website'
@ -36,12 +36,12 @@ const Options: FC<Props> = ({
label={t(`${I18N_PREFIX}.crawlSubPage`)}
isChecked={payload.crawl_sub_pages}
onChange={handleChange('crawl_sub_pages')}
labelClassName='text-[13px] leading-[16px] font-medium text-text-secondary'
testId='crawl-sub-pages'
labelClassName="text-[13px] leading-[16px] font-medium text-text-secondary"
testId="crawl-sub-pages"
/>
<div className='flex justify-between space-x-4'>
<div className="flex justify-between space-x-4">
<Field
className='shrink-0 grow'
className="shrink-0 grow"
label={t(`${I18N_PREFIX}.limit`)}
value={payload.limit}
onChange={handleChange('limit')}
@ -49,7 +49,7 @@ const Options: FC<Props> = ({
isRequired
/>
<Field
className='shrink-0 grow'
className="shrink-0 grow"
label={t(`${I18N_PREFIX}.maxDepth`)}
value={payload.max_depth}
onChange={handleChange('max_depth')}
@ -58,28 +58,28 @@ const Options: FC<Props> = ({
/>
</div>
<div className='flex justify-between space-x-4'>
<div className="flex justify-between space-x-4">
<Field
className='shrink-0 grow'
className="shrink-0 grow"
label={t(`${I18N_PREFIX}.excludePaths`)}
value={payload.excludes}
onChange={handleChange('excludes')}
placeholder='blog/*, /about/*'
placeholder="blog/*, /about/*"
/>
<Field
className='shrink-0 grow'
className="shrink-0 grow"
label={t(`${I18N_PREFIX}.includeOnlyPaths`)}
value={payload.includes}
onChange={handleChange('includes')}
placeholder='articles/*'
placeholder="articles/*"
/>
</div>
<CheckboxWithLabel
label={t(`${I18N_PREFIX}.extractOnlyMainContent`)}
isChecked={payload.only_main_content}
onChange={handleChange('only_main_content')}
labelClassName='text-[13px] leading-[16px] font-medium text-text-secondary'
testId='only-main-content'
labelClassName="text-[13px] leading-[16px] font-medium text-text-secondary"
testId="only-main-content"
/>
</div>
)