Initial commit

This commit is contained in:
John Wang
2023-05-15 08:51:32 +08:00
commit db896255d6
744 changed files with 56028 additions and 0 deletions

View File

@@ -0,0 +1,382 @@
.pageHeader {
@apply px-16;
position: sticky;
top: 0;
left: 0;
padding-top: 42px;
padding-bottom: 12px;
background-color: #fff;
font-weight: 600;
font-size: 18px;
line-height: 28px;
color: #101828;
z-index: 10;
}
.fixed {
background: rgba(255, 255, 255, 0.9);
border-bottom: 0.5px solid #EAECF0;
backdrop-filter: blur(4px);
}
.form {
@apply px-16 pb-8;
}
.form .label {
@apply pt-6 pb-2;
font-weight: 500;
font-size: 16px;
line-height: 24px;
color: #344054;
}
.segmentationItem {
min-height: 68px;
}
.indexItem {
min-height: 146px;
}
.indexItem .disableMask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 12px;
z-index: 2;
}
.indexItem .warningTip {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 8px 20px 8px 40px;
background: #FFFAEB;
border-top: 0.5px solid #FEF0C7;
border-radius: 12px;
font-size: 12px;
line-height: 18px;
color: #344054;
z-index: 3;
}
.indexItem .warningTip::before {
content: '';
position: absolute;
top: 11px;
left: 20px;
width: 12px;
height: 12px;
background: center no-repeat url(../assets/alert-triangle.svg);
background-size: 12px;
}
.indexItem .warningTip .click {
color: #155EEF;
cursor: pointer;
}
.indexItem.disabled:hover {
background-color: #fcfcfd;
border-color: #f2f4f7;
box-shadow: none;
cursor: default;
}
.indexItem.disabled:hover .radio {
@apply w-4 h-4 border-[2px] border-gray-200 rounded-full;
}
.radioItem {
@apply relative mb-2 rounded-xl border border-gray-100 cursor-pointer;
background-color: #fcfcfd;
}
.radioItem.segmentationItem.custom {
height: auto;
}
.radioItem.segmentationItem.custom .typeHeader {
/* height: 65px; */
}
.radioItem.indexItem .typeHeader {
@apply py-4 pr-5;
}
.radioItem.indexItem.active .typeHeader {
padding: 15.5px 19.5px 15.5px 63.5px;
}
.radioItem.indexItem .radio {
top: 16px;
right: 20px;
}
.radioItem.indexItem.active .radio {
top: 16px;
right: 19.5px;
}
.radioItem.indexItem .typeHeader .title {
@apply pb-1;
}
.radioItem.indexItem .typeHeader .tip {
@apply pb-3;
}
.radioItem .typeIcon {
position: absolute;
top: 18px;
left: 20px;
width: 32px;
height: 32px;
background: #EEF4FF center no-repeat;
border-radius: 8px;
}
.typeIcon.auto {
background-color: #F5F3FF;
background-image: url(../assets/zap-fast.svg);
}
.typeIcon.customize {
background-image: url(../assets/sliders-02.svg);
}
.typeIcon.qualified {
background-color: #FFF6ED;
background-image: url(../assets/star-07.svg);
}
.typeIcon.economical {
background-image: url(../assets/piggy-bank-01.svg);
}
.radioItem .radio {
@apply w-4 h-4 border-[2px] border-gray-200 rounded-full;
position: absolute;
top: 26px;
right: 20px;
}
.radioItem:hover {
background-color: #ffffff;
border-color: #B2CCFF;
box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
}
.radioItem:hover .radio {
border-color: #155eef;
}
.radioItem.active {
border-width: 1.5px;
border-color: #528BFF;
box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), 0px 1px 2px rgba(16, 24, 40, 0.06);
}
.radioItem.active .radio {
top: 25.5px;
right: 19.5px;
border-width: 5px;
border-color: #155EEF;
}
.radioItem.active:hover {
border-width: 1.5px;
border-color: #528BFF;
box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), 0px 1px 2px rgba(16, 24, 40, 0.06);
}
.radioItem.active .typeIcon {
top: 17.5px;
left: 19.5px;
}
.radioItem.active .typeHeader {
padding: 11.5px 63.5px;
}
.typeHeader {
@apply flex flex-col px-16 py-3 justify-center;
}
.typeHeader .title {
display: flex;
align-items: center;
padding-bottom: 2px;
font-weight: 500;
font-size: 16px;
line-height: 24px;
color: #101828;
}
.typeHeader .tip {
font-weight: 400;
font-size: 13px;
line-height: 18px;
color: #667085;
}
.recommendTag {
display: inline-flex;
justify-content: center;
align-items: center;
padding: 0 6px;
margin-left: 4px;
border: 1px solid #E0EAFF;
border-radius: 6px;
font-weight: 500;
font-size: 12px;
line-height: 20px;
color: #444CE7;
}
.typeFormBody {
@apply px-16;
border-top: 1px solid #F2F4F7;
}
.formRow {
@apply flex justify-between mt-6;
}
.formRow .label {
@apply mb-2 p-0;
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: #101828;
}
.ruleItem {
@apply flex items-center h-7;
}
.formFooter {
padding: 16px 0 28px;
}
.formFooter .button {
font-size: 13px;
line-height: 18px;
}
.input {
@apply inline-flex h-9 w-full py-1 px-2 rounded-lg text-xs leading-normal;
@apply bg-gray-100 caret-primary-600 hover:bg-gray-100 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none focus:bg-white placeholder:text-gray-400;
}
.file {
@apply flex justify-between items-center mt-8 px-6 py-4 rounded-xl bg-gray-50;
}
.file .divider {
@apply shrink-0 mx-4 w-px bg-gray-200;
height: 42px;
}
.fileIcon {
@apply inline-flex mr-1 w-6 h-6 bg-center bg-no-repeat;
background-image: url(../assets/pdf.svg);
background-size: 24px;
}
.fileIcon.pdf {
background-image: url(../assets/pdf.svg);
}
.fileIcon.html,
.fileIcon.htm {
background-image: url(../assets/html.svg);
}
.fileIcon.md,
.fileIcon.markdown {
background-image: url(../assets/md.svg);
}
.fileIcon.txt {
background-image: url(../assets/txt.svg);
}
.fileIcon.json {
background-image: url(../assets/json.svg);
}
.fileContent {
flex: 1 1 50%;
}
.divider {
@apply mx-3 w-px h-4 bg-gray-200;
}
.calculating {
color: #98A2B3;
font-size: 12px;
line-height: 18px;
}
.sideTip {
@apply flex flex-col items-center shrink-0;
padding-top: 108px;
width: 524px;
border-left: 0.5px solid #F2F4F7;
}
.tipCard {
@apply flex flex-col items-start p-6;
width: 320px;
background-color: #F9FAFB;
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
border-radius: 12px;
}
.tipCard .icon {
width: 32px;
height: 32px;
border: 1px solid #EAECF0;
border-radius: 6px;
background: center no-repeat url(../assets/book-open-01.svg);
background-size: 16px;
}
.tipCard .title {
margin: 12px 0;
font-weight: 500;
font-size: 16px;
line-height: 24px;
color: #344054;
}
.tipCard .content {
font-weight: 400;
font-size: 14px;
line-height: 20px;
color: #344054;
}
.previewWrap {
flex-shrink: 0;
width: 524px;
}
.previewHeader {
position: sticky;
top: 0;
left: 0;
padding-top: 42px;
background-color: #fff;
font-weight: 600;
font-size: 18px;
line-height: 28px;
color: #101828;
z-index: 10;
}

View File

@@ -0,0 +1,491 @@
'use client'
import React, { useState, useRef, useEffect, useLayoutEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import type { File, PreProcessingRule, Rules, FileIndexingEstimateResponse as IndexingEstimateResponse } from '@/models/datasets'
import {
fetchDefaultProcessRule,
createFirstDocument,
createDocument,
fetchFileIndexingEstimate as didFetchFileIndexingEstimate,
} from '@/service/datasets'
import type { CreateDocumentReq, createDocumentResponse } from '@/models/datasets'
import Button from '@/app/components/base/button'
import PreviewItem from './preview-item'
import Loading from '@/app/components/base/loading'
import { XMarkIcon } from '@heroicons/react/20/solid'
import cn from 'classnames'
import s from './index.module.css'
import Link from 'next/link'
import Toast from '@/app/components/base/toast'
import { formatNumber } from '@/utils/format'
type StepTwoProps = {
hasSetAPIKEY: boolean,
onSetting: () => void,
datasetId?: string,
indexingType?: string,
file?: File,
onStepChange: (delta: number) => void,
updateIndexingTypeCache: (type: string) => void,
updateResultCache: (res: createDocumentResponse) => void
}
enum SegmentType {
AUTO = 'automatic',
CUSTOM = 'custom',
}
enum IndexingType {
QUALIFIED = 'high_quality',
ECONOMICAL = 'economy',
}
const StepTwo = ({
hasSetAPIKEY,
onSetting,
datasetId,
indexingType,
file,
onStepChange,
updateIndexingTypeCache,
updateResultCache,
}: StepTwoProps) => {
const { t } = useTranslation()
const scrollRef = useRef<HTMLDivElement>(null)
const [scrolled, setScrolled] = useState(false)
const previewScrollRef = useRef<HTMLDivElement>(null)
const [previewScrolled, setPreviewScrolled] = useState(false)
const [segmentationType, setSegmentationType] = useState<SegmentType>(SegmentType.AUTO)
const [segmentIdentifier, setSegmentIdentifier] = useState('\\n')
const [max, setMax] = useState(1000)
const [rules, setRules] = useState<PreProcessingRule[]>([])
const [defaultConfig, setDefaultConfig] = useState<Rules>()
const hasSetIndexType = !!indexingType
const [indexType, setIndexType] = useState<IndexingType>(
indexingType ||
hasSetAPIKEY ? IndexingType.QUALIFIED : IndexingType.ECONOMICAL
)
const [showPreview, { setTrue: setShowPreview, setFalse: hidePreview }] = useBoolean()
const [customFileIndexingEstimate, setCustomFileIndexingEstimate] = useState<IndexingEstimateResponse | null>(null)
const [automaticFileIndexingEstimate, setAutomaticFileIndexingEstimate] = useState<IndexingEstimateResponse | null>(null)
const fileIndexingEstimate = (() => {
return segmentationType === SegmentType.AUTO ? automaticFileIndexingEstimate : customFileIndexingEstimate
})()
const scrollHandle = (e: any) => {
if (e.target.scrollTop > 0) {
setScrolled(true)
} else {
setScrolled(false)
}
}
const previewScrollHandle = (e: any) => {
if (e.target.scrollTop > 0) {
setPreviewScrolled(true)
} else {
setPreviewScrolled(false)
}
}
const getFileName = (name: string) => {
const arr = name.split('.')
return arr.slice(0, -1).join('.')
}
const getRuleName = (key: string) => {
if (key === 'remove_extra_spaces') {
return t('datasetCreation.stepTwo.removeExtraSpaces')
}
if (key === 'remove_urls_emails') {
return t('datasetCreation.stepTwo.removeUrlEmails')
}
if (key === 'remove_stopwords') {
return t('datasetCreation.stepTwo.removeStopwords')
}
}
const ruleChangeHandle = (id: string) => {
const newRules = rules.map(rule => {
if (rule.id === id) {
return {
id: rule.id,
enabled: !rule.enabled,
}
}
return rule
})
setRules(newRules)
}
const resetRules = () => {
if (defaultConfig) {
setSegmentIdentifier(defaultConfig.segmentation.separator === '\n' ? '\\n' : defaultConfig.segmentation.separator || '\\n')
setMax(defaultConfig.segmentation.max_tokens)
setRules(defaultConfig.pre_processing_rules)
}
}
const confirmChangeCustomConfig = async () => {
setCustomFileIndexingEstimate(null)
setShowPreview()
await fetchFileIndexingEstimate()
}
const getIndexing_technique = () => indexingType ? indexingType : indexType
const getProcessRule = () => {
const processRule: any = {
rules: {}, // api will check this. It will be removed after api refactored.
mode: segmentationType,
}
if (segmentationType === SegmentType.CUSTOM) {
const ruleObj = {
pre_processing_rules: rules,
segmentation: {
separator: segmentIdentifier === '\\n' ? '\n' : segmentIdentifier,
max_tokens: max,
},
}
processRule.rules = ruleObj
}
return processRule
}
const getFileIndexingEstimateParams = () => {
const params = {
file_id: file?.id,
dataset_id: datasetId,
indexing_technique: getIndexing_technique(),
process_rule: getProcessRule(),
}
return params
}
const fetchFileIndexingEstimate = async () => {
const res = await didFetchFileIndexingEstimate(getFileIndexingEstimateParams())
if (segmentationType === SegmentType.CUSTOM) {
setCustomFileIndexingEstimate(res)
}
else {
setAutomaticFileIndexingEstimate(res)
}
}
const getCreationParams = () => {
const params = {
data_source: {
type: 'upload_file',
info: file?.id,
name: file?.name,
},
indexing_technique: getIndexing_technique(),
process_rule: getProcessRule(),
} as CreateDocumentReq
return params
}
const getRules = async () => {
try {
const res = await fetchDefaultProcessRule({ url: '/datasets/process-rule' })
const separator = res.rules.segmentation.separator
setSegmentIdentifier(separator === '\n' ? '\\n' : separator || '\\n')
setMax(res.rules.segmentation.max_tokens)
setRules(res.rules.pre_processing_rules)
setDefaultConfig(res.rules)
}
catch (err) {
console.log(err)
}
}
const createHandle = async () => {
try {
let res;
const params = getCreationParams()
if (!datasetId) {
res = await createFirstDocument({
body: params
})
updateIndexingTypeCache(indexType)
updateResultCache(res)
} else {
res = await createDocument({
datasetId,
body: params
})
updateIndexingTypeCache(indexType)
updateResultCache({
document: res,
})
}
onStepChange(+1)
}
catch (err) {
Toast.notify({
type: 'error',
message: err + '',
})
}
}
useEffect(() => {
// fetch rules
getRules()
}, [])
useEffect(() => {
scrollRef.current?.addEventListener('scroll', scrollHandle);
return () => {
scrollRef.current?.removeEventListener('scroll', scrollHandle);
}
}, [])
useLayoutEffect(() => {
if (showPreview) {
previewScrollRef.current?.addEventListener('scroll', previewScrollHandle);
return () => {
previewScrollRef.current?.removeEventListener('scroll', previewScrollHandle);
}
}
}, [showPreview])
useEffect(() => {
// get indexing type by props
if (indexingType) {
setIndexType(indexingType as IndexingType)
} else {
setIndexType(hasSetAPIKEY ? IndexingType.QUALIFIED : IndexingType.ECONOMICAL)
}
}, [hasSetAPIKEY, indexingType, datasetId])
useEffect(() => {
if (segmentationType === SegmentType.AUTO) {
setAutomaticFileIndexingEstimate(null)
setShowPreview()
fetchFileIndexingEstimate()
} else {
hidePreview()
setCustomFileIndexingEstimate(null)
}
}, [segmentationType, indexType])
return (
<div className='flex w-full h-full'>
<div ref={scrollRef} className='relative h-full w-full overflow-y-scroll'>
<div className={cn(s.pageHeader, scrolled && s.fixed)}>{t('datasetCreation.steps.two')}</div>
<div className={cn(s.form)}>
<div className={s.label}>{t('datasetCreation.stepTwo.segmentation')}</div>
<div className='max-w-[640px]'>
<div
className={cn(
s.radioItem,
s.segmentationItem,
segmentationType === SegmentType.AUTO && s.active
)}
onClick={() => setSegmentationType(SegmentType.AUTO)}
>
<span className={cn(s.typeIcon, s.auto)} />
<span className={cn(s.radio)} />
<div className={s.typeHeader}>
<div className={s.title}>{t('datasetCreation.stepTwo.auto')}</div>
<div className={s.tip}>{t('datasetCreation.stepTwo.autoDescription')}</div>
</div>
</div>
<div
className={cn(
s.radioItem,
s.segmentationItem,
segmentationType === SegmentType.CUSTOM && s.active,
segmentationType === SegmentType.CUSTOM && s.custom,
)}
onClick={() => setSegmentationType(SegmentType.CUSTOM)}
>
<span className={cn(s.typeIcon, s.customize)} />
<span className={cn(s.radio)} />
<div className={s.typeHeader}>
<div className={s.title}>{t('datasetCreation.stepTwo.custom')}</div>
<div className={s.tip}>{t('datasetCreation.stepTwo.customDescription')}</div>
</div>
{segmentationType === SegmentType.CUSTOM && (
<div className={s.typeFormBody}>
<div className={s.formRow}>
<div className='w-full'>
<div className={s.label}>{t('datasetCreation.stepTwo.separator')}</div>
<input
type="text"
className={s.input}
placeholder={t('datasetCreation.stepTwo.separatorPlaceholder') || ''} value={segmentIdentifier}
onChange={(e) => setSegmentIdentifier(e.target.value)}
/>
</div>
</div>
<div className={s.formRow}>
<div className='w-full'>
<div className={s.label}>{t('datasetCreation.stepTwo.maxLength')}</div>
<input
type="number"
className={s.input}
placeholder={t('datasetCreation.stepTwo.separatorPlaceholder') || ''} value={max}
onChange={(e) => setMax(Number(e.target.value))}
/>
</div>
</div>
<div className={s.formRow}>
<div className='w-full'>
<div className={s.label}>{t('datasetCreation.stepTwo.rules')}</div>
{rules.map(rule => (
<div key={rule.id} className={s.ruleItem}>
<input id={rule.id} type="checkbox" defaultChecked={rule.enabled} onChange={() => ruleChangeHandle(rule.id)} className="w-4 h-4 rounded border-gray-300 text-blue-700 focus:ring-blue-700" />
<label htmlFor={rule.id} className="ml-2 text-sm font-normal cursor-pointer text-gray-800">{getRuleName(rule.id)}</label>
</div>
))}
</div>
</div>
<div className={s.formFooter}>
<Button type="primary" className={cn(s.button, '!h-8 text-primary-600')} onClick={confirmChangeCustomConfig}>{t('datasetCreation.stepTwo.preview')}</Button>
<Button className={cn(s.button, 'ml-2 !h-8')} onClick={resetRules}>{t('datasetCreation.stepTwo.reset')}</Button>
</div>
</div>
)}
</div>
</div>
<div className={s.label}>{t('datasetCreation.stepTwo.indexMode')}</div>
<div className='max-w-[640px]'>
<div className='flex items-center gap-3'>
{(!hasSetIndexType || (hasSetIndexType && indexingType === IndexingType.QUALIFIED)) && (
<div
className={cn(
s.radioItem,
s.indexItem,
!hasSetAPIKEY && s.disabled,
!hasSetIndexType && indexType === IndexingType.QUALIFIED && s.active,
hasSetIndexType && s.disabled,
hasSetIndexType && '!w-full',
)}
onClick={() => {
if (hasSetAPIKEY) {
setIndexType(IndexingType.QUALIFIED)
}
}}
>
<span className={cn(s.typeIcon, s.qualified)} />
{!hasSetIndexType && <span className={cn(s.radio)} />}
<div className={s.typeHeader}>
<div className={s.title}>
{t('datasetCreation.stepTwo.qualified')}
{!hasSetIndexType && <span className={s.recommendTag}>{t('datasetCreation.stepTwo.recommend')}</span>}
</div>
<div className={s.tip}>{t('datasetCreation.stepTwo.qualifiedTip')}</div>
<div className='pb-0.5 text-xs font-medium text-gray-500'>{t('datasetCreation.stepTwo.emstimateCost')}</div>
{
!!fileIndexingEstimate ? (
<div className='text-xs font-medium text-gray-800'>{formatNumber(fileIndexingEstimate.tokens)} tokens(<span className='text-yellow-500'>${formatNumber(fileIndexingEstimate.total_price)}</span>)</div>
) : (
<div className={s.calculating}>{t('datasetCreation.stepTwo.calculating')}</div>
)
}
</div>
{!hasSetAPIKEY && (
<div className={s.warningTip}>
<span>{t('datasetCreation.stepTwo.warning')}&nbsp;</span>
<span className={s.click} onClick={onSetting}>{t('datasetCreation.stepTwo.click')}</span>
</div>
)}
</div>
)}
{(!hasSetIndexType || (hasSetIndexType && indexingType === IndexingType.ECONOMICAL)) && (
<div
className={cn(
s.radioItem,
s.indexItem,
!hasSetIndexType && indexType === IndexingType.ECONOMICAL && s.active,
hasSetIndexType && s.disabled,
hasSetIndexType && '!w-full',
)}
onClick={() => !hasSetIndexType && setIndexType(IndexingType.ECONOMICAL)}
>
<span className={cn(s.typeIcon, s.economical)} />
{!hasSetIndexType && <span className={cn(s.radio)} />}
<div className={s.typeHeader}>
<div className={s.title}>{t('datasetCreation.stepTwo.economical')}</div>
<div className={s.tip}>{t('datasetCreation.stepTwo.economicalTip')}</div>
<div className='pb-0.5 text-xs font-medium text-gray-500'>{t('datasetCreation.stepTwo.emstimateCost')}</div>
<div className='text-xs font-medium text-gray-800'>0 tokens</div>
</div>
</div>
)}
</div>
{hasSetIndexType && (
<div className='mt-2 text-xs text-gray-500 font-medium'>
{t('datasetCreation.stepTwo.indexSettedTip')}
<Link className='text-[#155EEF]' href={`/datasets/${datasetId}/settings`}>{t('datasetCreation.stepTwo.datasetSettingLink')}</Link>
</div>
)}
<div className={s.file}>
<div className={s.fileContent}>
<div className='mb-2 text-xs font-medium text-gray-500'>{t('datasetCreation.stepTwo.fileName')}</div>
<div className='flex items-center text-sm leading-6 font-medium text-gray-800'>
<span className={cn(s.fileIcon, file && s[file.extension])} />
{getFileName(file?.name || '')}
</div>
</div>
<div className={s.divider} />
<div className={s.fileContent}>
<div className='mb-2 text-xs font-medium text-gray-500'>{t('datasetCreation.stepTwo.emstimateSegment')}</div>
<div className='flex items-center text-sm leading-6 font-medium text-gray-800'>
{
!!fileIndexingEstimate ? (
<div className='text-xs font-medium text-gray-800'>{formatNumber(fileIndexingEstimate.total_segments)} </div>
) : (
<div className={s.calculating}>{t('datasetCreation.stepTwo.calculating')}</div>
)
}
</div>
</div>
</div>
<div className='flex items-center mt-8 py-2'>
<Button onClick={() => onStepChange(-1)}>{t('datasetCreation.stepTwo.lastStep')}</Button>
<div className={s.divider} />
<Button type='primary' onClick={createHandle}>{t('datasetCreation.stepTwo.nextStep')}</Button>
</div>
</div>
</div>
</div>
{(showPreview) ? (
<div ref={previewScrollRef} className={cn(s.previewWrap, 'relativeh-full overflow-y-scroll border-l border-[#F2F4F7]')}>
<div className={cn(s.previewHeader, previewScrolled && `${s.fixed} pb-3`, ' flex items-center justify-between px-8')}>
<span>{t('datasetCreation.stepTwo.previewTitle')}</span>
<div className='flex items-center justify-center w-6 h-6 cursor-pointer' onClick={hidePreview}>
<XMarkIcon className='h-4 w-4'></XMarkIcon>
</div>
</div>
<div className='my-4 px-8 space-y-4'>
{fileIndexingEstimate?.preview ? (
<>
{fileIndexingEstimate?.preview.map((item, index) => (
<PreviewItem key={item} content={item} index={index + 1} />
))}
</>
) : <div className='flex items-center justify-center h-[200px]'><Loading type='area'></Loading></div>
}
</div>
</div>
) :
(<div className={cn(s.sideTip)}>
<div className={s.tipCard}>
<span className={s.icon} />
<div className={s.title}>{t('datasetCreation.stepTwo.sideTipTitle')}</div>
<div className={s.content}>
<p className='mb-3'>{t('datasetCreation.stepTwo.sideTipP1')}</p>
<p className='mb-3'>{t('datasetCreation.stepTwo.sideTipP2')}</p>
<p className='mb-3'>{t('datasetCreation.stepTwo.sideTipP3')}</p>
<p>{t('datasetCreation.stepTwo.sideTipP4')}</p>
</div>
</div>
</div>)}
</div>
)
}
export default StepTwo

View File

@@ -0,0 +1,49 @@
'use client'
import React, { FC } from 'react'
import { useTranslation } from 'react-i18next'
export interface IPreviewItemProps {
index: number
content: string
}
const sharpIcon = (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.74999 1.5L3.24999 10.5M8.74998 1.5L7.24998 10.5M10.25 4H1.75M9.75 8H1.25" stroke="#98A2B3" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
const textIcon = (
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 3.5H8M6 3.5V8.5M3.9 10.5H8.1C8.94008 10.5 9.36012 10.5 9.68099 10.3365C9.96323 10.1927 10.1927 9.96323 10.3365 9.68099C10.5 9.36012 10.5 8.94008 10.5 8.1V3.9C10.5 3.05992 10.5 2.63988 10.3365 2.31901C10.1927 2.03677 9.96323 1.8073 9.68099 1.66349C9.36012 1.5 8.94008 1.5 8.1 1.5H3.9C3.05992 1.5 2.63988 1.5 2.31901 1.66349C2.03677 1.8073 1.8073 2.03677 1.66349 2.31901C1.5 2.63988 1.5 3.05992 1.5 3.9V8.1C1.5 8.94008 1.5 9.36012 1.66349 9.68099C1.8073 9.96323 2.03677 10.1927 2.31901 10.3365C2.63988 10.5 3.05992 10.5 3.9 10.5Z" stroke="#667085" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
const PreviewItem: FC<IPreviewItemProps> = ({
index,
content,
}) => {
const { t } = useTranslation()
const charNums = (content || '').length
const formatedIndex = (() => (index + '').padStart(3, '0'))()
return (
<div className='p-4 rounded-xl bg-gray-50'>
<div className='flex items-center justify-between h-5 text-xs text-gray-500'>
<div className='flex items-center h-[18px] space-x-1 border border-gray-200 box-border rounded-md italic pl-1 pr-1.5 font-medium'>
{sharpIcon}
<span>{formatedIndex}</span>
</div>
<div className='flex items-center space-x-1'>
{textIcon}
<span>{charNums} {t('datasetCreation.stepTwo.characters')}</span>
</div>
</div>
<div className='mt-2 max-h-[120px] line-clamp-6 overflow-hidden text-sm text-gray-800'>
{content}
</div>
</div>
)
}
export default React.memo(PreviewItem)