refactor: refactor preview components
parent
69a60101fe
commit
faf6b9ea03
@ -0,0 +1,62 @@
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { useDatasourceOptions } from '../hooks'
|
||||
import OptionCard from './option-card'
|
||||
import { File, Watercrawl } from '@/app/components/base/icons/src/public/knowledge'
|
||||
import { Notion } from '@/app/components/base/icons/src/public/common'
|
||||
import { Jina } from '@/app/components/base/icons/src/public/llm'
|
||||
import { DataSourceType } from '@/models/datasets'
|
||||
import { DataSourceProvider } from '@/models/common'
|
||||
import type { Datasource } from '@/app/components/rag-pipeline/components/panel/test-run/types'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
|
||||
|
||||
type DataSourceOptionsProps = {
|
||||
pipelineNodes: Node<DataSourceNodeType>[]
|
||||
datasourceNodeId: string
|
||||
onSelect: (option: Datasource) => void
|
||||
}
|
||||
|
||||
const DATA_SOURCE_ICONS = {
|
||||
[DataSourceType.FILE]: File as React.FC<React.SVGProps<SVGSVGElement>>,
|
||||
[DataSourceType.NOTION]: Notion as React.FC<React.SVGProps<SVGSVGElement>>,
|
||||
[DataSourceProvider.fireCrawl]: '🔥',
|
||||
[DataSourceProvider.jinaReader]: Jina as React.FC<React.SVGProps<SVGSVGElement>>,
|
||||
[DataSourceProvider.waterCrawl]: Watercrawl as React.FC<React.SVGProps<SVGSVGElement>>,
|
||||
}
|
||||
|
||||
const DataSourceOptions = ({
|
||||
pipelineNodes,
|
||||
datasourceNodeId,
|
||||
onSelect,
|
||||
}: DataSourceOptionsProps) => {
|
||||
const { datasources, options } = useDatasourceOptions(pipelineNodes)
|
||||
|
||||
const handelSelect = useCallback((value: string) => {
|
||||
const selectedOption = datasources.find(option => option.nodeId === value)
|
||||
if (!selectedOption)
|
||||
return
|
||||
onSelect(selectedOption)
|
||||
}, [datasources, onSelect])
|
||||
|
||||
useEffect(() => {
|
||||
if (options.length > 0)
|
||||
handelSelect(options[0].value)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='grid w-full grid-cols-4 gap-1'>
|
||||
{options.map(option => (
|
||||
<OptionCard
|
||||
key={option.value}
|
||||
label={option.label}
|
||||
selected={datasourceNodeId === option.value}
|
||||
Icon={DATA_SOURCE_ICONS[option.type as keyof typeof DATA_SOURCE_ICONS]}
|
||||
onClick={handelSelect.bind(null, option.value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DataSourceOptions
|
||||
@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type OptionCardProps = {
|
||||
label: string
|
||||
Icon: React.FC<React.SVGProps<SVGSVGElement>> | string
|
||||
selected: boolean
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const OptionCard = ({
|
||||
label,
|
||||
Icon,
|
||||
selected,
|
||||
onClick,
|
||||
}: OptionCardProps) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-xl border border-components-option-card-option-border bg-components-option-card-option-bg p-3 shadow-shadow-shadow-3',
|
||||
selected
|
||||
? 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs ring-[0.5px] ring-inset ring-components-option-card-option-selected-border'
|
||||
: 'hover:bg-components-option-card-bg-hover hover:border-components-option-card-option-border-hover hover:shadow-xs',
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className='flex size-8 items-center justify-center rounded-lg border-[0.5px] border-components-panel-border bg-background-default-dodge p-1.5'>
|
||||
{
|
||||
typeof Icon === 'string'
|
||||
? <div className='text-[18px] leading-[18px]'>{Icon}</div>
|
||||
: <Icon className='size-5' />
|
||||
}
|
||||
</div>
|
||||
<div className={cn('system-sm-medium text-text-secondary', selected && 'text-primary')}>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(OptionCard)
|
||||
@ -0,0 +1,44 @@
|
||||
import React from 'react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { RiArrowRightLine } from '@remixicon/react'
|
||||
|
||||
type ActionsProps = {
|
||||
disabled?: boolean
|
||||
handleNextStep: () => void
|
||||
}
|
||||
|
||||
const Actions = ({
|
||||
disabled,
|
||||
handleNextStep,
|
||||
}: ActionsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { datasetId } = useParams()
|
||||
|
||||
return (
|
||||
<div className='flex justify-end gap-x-2'>
|
||||
<a
|
||||
href={`/datasets/${datasetId}/documents`}
|
||||
>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='px-3 py-2'
|
||||
>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
</a>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
variant='primary'
|
||||
onClick={handleNextStep}
|
||||
className='gap-x-0.5'
|
||||
>
|
||||
<span className='px-0.5'>{t('datasetCreation.stepOne.button')}</span>
|
||||
<RiArrowRightLine className='size-4' />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Actions)
|
||||
@ -0,0 +1,78 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Loading from './loading'
|
||||
import type { CustomFile as File } from '@/models/datasets'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useFilePreview } from '@/service/use-common'
|
||||
import DocumentFileIcon from '../../../common/document-file-icon'
|
||||
import { formatNumberAbbreviated } from '@/utils/format'
|
||||
|
||||
type FilePreviewProps = {
|
||||
file: File
|
||||
hidePreview: () => void
|
||||
}
|
||||
|
||||
const FilePreview = ({
|
||||
file,
|
||||
hidePreview,
|
||||
}: FilePreviewProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { data: fileData, isFetching } = useFilePreview(file.id || '')
|
||||
|
||||
const getFileName = (currentFile?: File) => {
|
||||
if (!currentFile)
|
||||
return ''
|
||||
const arr = currentFile.name.split('.')
|
||||
return arr.slice(0, -1).join()
|
||||
}
|
||||
|
||||
const getFileSize = (size: number) => {
|
||||
if (size / 1024 < 10)
|
||||
return `${(size / 1024).toFixed(1)} KB`
|
||||
|
||||
return `${(size / 1024 / 1024).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='h-full rounded-t-xl border-l border-t border-components-panel-border bg-background-default-lighter shadow-md shadow-shadow-shadow-5'>
|
||||
<div className='flex gap-x-2 pb-3 pl-6 pr-4 pt-4'>
|
||||
<div className='flex grow flex-col gap-y-1'>
|
||||
<div className='system-2xs-semibold-uppercase'>{t('datasetPipeline.addDocuments.stepOne.preview')}</div>
|
||||
<div className='title-md-semi-bold text-tex-primary'>{`${getFileName(file)}.${file.extension}`}</div>
|
||||
<div className='system-xs-medium flex gap-x-1 text-text-tertiary'>
|
||||
<DocumentFileIcon
|
||||
className='size-6 shrink-0'
|
||||
name={file.name}
|
||||
extension={file.extension}
|
||||
/>
|
||||
<span className='uppercase'>{file.extension}</span>
|
||||
<span>·</span>
|
||||
<span>{getFileSize(file.size)}</span>
|
||||
{fileData && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span>{`${formatNumberAbbreviated(fileData.content.length)} ${t('datasetPipeline.addDocuments.characters')}`}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
className='flex h-8 w-8 shrink-0 items-center justify-center'
|
||||
onClick={hidePreview}
|
||||
>
|
||||
<RiCloseLine className='size-[18px]' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='px-6 py-5'>
|
||||
{isFetching && <Loading />}
|
||||
{!isFetching && fileData && (
|
||||
<div className='body-md-regular overflow-hidden text-text-secondary'>{fileData.content}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FilePreview
|
||||
@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
import { SkeletonContainer, SkeletonRectangle } from '@/app/components/base/skeleton'
|
||||
|
||||
const Loading = () => {
|
||||
return (
|
||||
<div className='flex h-full flex-col gap-y-12 bg-gradient-to-b from-components-panel-bg-transparent to-components-panel-bg px-6 py-5'>
|
||||
<SkeletonContainer className='w-full gap-0'>
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-3/5' />
|
||||
</SkeletonContainer>
|
||||
<SkeletonContainer className='w-full gap-0'>
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-[70%]' />
|
||||
</SkeletonContainer>
|
||||
<SkeletonContainer className='w-full gap-0'>
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-[56%]' />
|
||||
</SkeletonContainer>
|
||||
<SkeletonContainer className='w-full gap-0'>
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-3/5' />
|
||||
</SkeletonContainer>
|
||||
<SkeletonContainer className='w-full gap-0'>
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-3/5' />
|
||||
</SkeletonContainer>
|
||||
<SkeletonContainer className='w-full gap-0'>
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-full' />
|
||||
<SkeletonRectangle className='my-1.5 w-1/2' />
|
||||
</SkeletonContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Loading)
|
||||
@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import { usePreviewNotionPage } from '@/service/knowledge/use-dataset'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { formatNumberAbbreviated } from '@/utils/format'
|
||||
import Loading from './loading'
|
||||
import { Notion } from '@/app/components/base/icons/src/public/common'
|
||||
|
||||
type NotionPagePreviewProps = {
|
||||
currentPage: NotionPage
|
||||
hidePreview: () => void
|
||||
}
|
||||
|
||||
const NotionPagePreview = ({
|
||||
currentPage,
|
||||
hidePreview,
|
||||
}: NotionPagePreviewProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data: notionPageData, isFetching } = usePreviewNotionPage({
|
||||
workspaceID: currentPage.workspace_id,
|
||||
pageID: currentPage.page_id,
|
||||
pageType: currentPage.type,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='h-full rounded-t-xl border-l border-t border-components-panel-border bg-background-default-lighter shadow-md shadow-shadow-shadow-5'>
|
||||
<div className='flex gap-x-2 pb-3 pl-6 pr-4 pt-4'>
|
||||
<div className='flex grow flex-col gap-y-1'>
|
||||
<div className='system-2xs-semibold-uppercase'>{t('datasetPipeline.addDocuments.stepOne.preview')}</div>
|
||||
<div className='title-md-semi-bold text-tex-primary'>{currentPage?.page_name}</div>
|
||||
<div className='system-xs-medium flex gap-x-1 text-text-tertiary'>
|
||||
<Notion className='size-3.5' />
|
||||
<span>·</span>
|
||||
<span>Notion Page</span>
|
||||
<span>·</span>
|
||||
{notionPageData && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span>{`${formatNumberAbbreviated(notionPageData.content.length)} ${t('datasetPipeline.addDocuments.characters')}`}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
className='flex h-8 w-8 shrink-0 items-center justify-center'
|
||||
onClick={hidePreview}
|
||||
>
|
||||
<RiCloseLine className='size-[18px]' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='px-6 py-5'>
|
||||
{isFetching && <Loading />}
|
||||
{!isFetching && notionPageData && (
|
||||
<div className='body-md-regular overflow-hidden text-text-secondary'>{notionPageData.content}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotionPagePreview
|
||||
@ -0,0 +1,48 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { CrawlResultItem } from '@/models/datasets'
|
||||
import { RiCloseLine, RiGlobalLine } from '@remixicon/react'
|
||||
import { formatNumberAbbreviated } from '@/utils/format'
|
||||
|
||||
type WebsitePreviewProps = {
|
||||
payload: CrawlResultItem
|
||||
hidePreview: () => void
|
||||
}
|
||||
|
||||
const WebsitePreview = ({
|
||||
payload,
|
||||
hidePreview,
|
||||
}: WebsitePreviewProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='h-full rounded-t-xl border-l border-t border-components-panel-border bg-background-default-lighter shadow-md shadow-shadow-shadow-5'>
|
||||
<div className='flex gap-x-2 pb-3 pl-6 pr-4 pt-4'>
|
||||
<div className='flex grow flex-col gap-y-1'>
|
||||
<div className='system-2xs-semibold-uppercase'>{t('datasetPipeline.addDocuments.stepOne.preview')}</div>
|
||||
<div className='title-md-semi-bold text-tex-primary'>{payload.title}</div>
|
||||
<div className='system-xs-medium flex gap-x-1 text-text-tertiary'>
|
||||
<RiGlobalLine className='size-3.5' />
|
||||
<span className='uppercase' title={payload.source_url}>{payload.source_url}</span>
|
||||
<span>·</span>
|
||||
<span>·</span>
|
||||
<span>{`${formatNumberAbbreviated(payload.markdown.length)} ${t('datasetPipeline.addDocuments.characters')}`}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
className='flex h-8 w-8 shrink-0 items-center justify-center'
|
||||
onClick={hidePreview}
|
||||
>
|
||||
<RiCloseLine className='size-[18px]' />
|
||||
</button>
|
||||
</div>
|
||||
<div className='px-6 py-5'>
|
||||
<div className='body-md-regular overflow-hidden text-text-secondary'>{payload.markdown}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WebsitePreview
|
||||
Loading…
Reference in New Issue