Feature/mutil embedding model (#908)
Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: jyong <jyong@dify.ai> Co-authored-by: StyleZhang <jasonapring2015@outlook.com>pull/910/head
parent
4420281d96
commit
db7156dafd
@ -0,0 +1,32 @@
|
||||
"""add_qa_document_language
|
||||
|
||||
Revision ID: 2c8af9671032
|
||||
Revises: 8d2d099ceb74
|
||||
Create Date: 2023-08-01 18:57:27.294973
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '2c8af9671032'
|
||||
down_revision = '5022897aaceb'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('documents', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('doc_language', sa.String(length=255), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('documents', schema=None) as batch_op:
|
||||
batch_op.drop_column('doc_language')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@ -0,0 +1,34 @@
|
||||
"""add_dataset_model_name
|
||||
|
||||
Revision ID: e8883b0148c9
|
||||
Revises: 2c8af9671032
|
||||
Create Date: 2023-08-15 20:54:58.936787
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e8883b0148c9'
|
||||
down_revision = '2c8af9671032'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('datasets', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('embedding_model', sa.String(length=255), server_default=sa.text("'text-embedding-ada-002'::character varying"), nullable=False))
|
||||
batch_op.add_column(sa.Column('embedding_model_provider', sa.String(length=255), server_default=sa.text("'openai'::character varying"), nullable=False))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('datasets', schema=None) as batch_op:
|
||||
batch_op.drop_column('embedding_model_provider')
|
||||
batch_op.drop_column('embedding_model')
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@ -0,0 +1,69 @@
|
||||
|
||||
from typing import Optional, List
|
||||
|
||||
from langchain.schema import Document
|
||||
|
||||
from core.index.index import IndexBuilder
|
||||
|
||||
from models.dataset import Dataset, DocumentSegment
|
||||
|
||||
|
||||
class VectorService:
|
||||
|
||||
@classmethod
|
||||
def create_segment_vector(cls, keywords: Optional[List[str]], segment: DocumentSegment, dataset: Dataset):
|
||||
document = Document(
|
||||
page_content=segment.content,
|
||||
metadata={
|
||||
"doc_id": segment.index_node_id,
|
||||
"doc_hash": segment.index_node_hash,
|
||||
"document_id": segment.document_id,
|
||||
"dataset_id": segment.dataset_id,
|
||||
}
|
||||
)
|
||||
|
||||
# save vector index
|
||||
index = IndexBuilder.get_index(dataset, 'high_quality')
|
||||
if index:
|
||||
index.add_texts([document], duplicate_check=True)
|
||||
|
||||
# save keyword index
|
||||
index = IndexBuilder.get_index(dataset, 'economy')
|
||||
if index:
|
||||
if keywords and len(keywords) > 0:
|
||||
index.create_segment_keywords(segment.index_node_id, keywords)
|
||||
else:
|
||||
index.add_texts([document])
|
||||
|
||||
@classmethod
|
||||
def update_segment_vector(cls, keywords: Optional[List[str]], segment: DocumentSegment, dataset: Dataset):
|
||||
# update segment index task
|
||||
vector_index = IndexBuilder.get_index(dataset, 'high_quality')
|
||||
kw_index = IndexBuilder.get_index(dataset, 'economy')
|
||||
# delete from vector index
|
||||
if vector_index:
|
||||
vector_index.delete_by_ids([segment.index_node_id])
|
||||
|
||||
# delete from keyword index
|
||||
kw_index.delete_by_ids([segment.index_node_id])
|
||||
|
||||
# add new index
|
||||
document = Document(
|
||||
page_content=segment.content,
|
||||
metadata={
|
||||
"doc_id": segment.index_node_id,
|
||||
"doc_hash": segment.index_node_hash,
|
||||
"document_id": segment.document_id,
|
||||
"dataset_id": segment.dataset_id,
|
||||
}
|
||||
)
|
||||
|
||||
# save vector index
|
||||
if vector_index:
|
||||
vector_index.add_texts([document], duplicate_check=True)
|
||||
|
||||
# save keyword index
|
||||
if keywords and len(keywords) > 0:
|
||||
kw_index.create_segment_keywords(segment.index_node_id, keywords)
|
||||
else:
|
||||
kw_index.add_texts([document])
|
||||
@ -0,0 +1,95 @@
|
||||
import datetime
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
from typing import Optional, List
|
||||
|
||||
import click
|
||||
from celery import shared_task
|
||||
from sqlalchemy import func
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from core.index.index import IndexBuilder
|
||||
from core.indexing_runner import IndexingRunner
|
||||
from core.model_providers.model_factory import ModelFactory
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from libs import helper
|
||||
from models.dataset import DocumentSegment, Dataset, Document
|
||||
|
||||
|
||||
@shared_task(queue='dataset')
|
||||
def batch_create_segment_to_index_task(job_id: str, content: List, dataset_id: str, document_id: str,
|
||||
tenant_id: str, user_id: str):
|
||||
"""
|
||||
Async batch create segment to index
|
||||
:param job_id:
|
||||
:param content:
|
||||
:param dataset_id:
|
||||
:param document_id:
|
||||
:param tenant_id:
|
||||
:param user_id:
|
||||
|
||||
Usage: batch_create_segment_to_index_task.delay(segment_id)
|
||||
"""
|
||||
logging.info(click.style('Start batch create segment jobId: {}'.format(job_id), fg='green'))
|
||||
start_at = time.perf_counter()
|
||||
|
||||
indexing_cache_key = 'segment_batch_import_{}'.format(job_id)
|
||||
|
||||
try:
|
||||
dataset = db.session.query(Dataset).filter(Dataset.id == dataset_id).first()
|
||||
if not dataset:
|
||||
raise ValueError('Dataset not exist.')
|
||||
|
||||
dataset_document = db.session.query(Document).filter(Document.id == document_id).first()
|
||||
if not dataset_document:
|
||||
raise ValueError('Document not exist.')
|
||||
|
||||
if not dataset_document.enabled or dataset_document.archived or dataset_document.indexing_status != 'completed':
|
||||
raise ValueError('Document is not available.')
|
||||
document_segments = []
|
||||
for segment in content:
|
||||
content = segment['content']
|
||||
doc_id = str(uuid.uuid4())
|
||||
segment_hash = helper.generate_text_hash(content)
|
||||
embedding_model = ModelFactory.get_embedding_model(
|
||||
tenant_id=dataset.tenant_id,
|
||||
model_provider_name=dataset.embedding_model_provider,
|
||||
model_name=dataset.embedding_model
|
||||
)
|
||||
|
||||
# calc embedding use tokens
|
||||
tokens = embedding_model.get_num_tokens(content)
|
||||
max_position = db.session.query(func.max(DocumentSegment.position)).filter(
|
||||
DocumentSegment.document_id == dataset_document.id
|
||||
).scalar()
|
||||
segment_document = DocumentSegment(
|
||||
tenant_id=tenant_id,
|
||||
dataset_id=dataset_id,
|
||||
document_id=document_id,
|
||||
index_node_id=doc_id,
|
||||
index_node_hash=segment_hash,
|
||||
position=max_position + 1 if max_position else 1,
|
||||
content=content,
|
||||
word_count=len(content),
|
||||
tokens=tokens,
|
||||
created_by=user_id,
|
||||
indexing_at=datetime.datetime.utcnow(),
|
||||
status='completed',
|
||||
completed_at=datetime.datetime.utcnow()
|
||||
)
|
||||
if dataset_document.doc_form == 'qa_model':
|
||||
segment_document.answer = segment['answer']
|
||||
db.session.add(segment_document)
|
||||
document_segments.append(segment_document)
|
||||
# add index to db
|
||||
indexing_runner = IndexingRunner()
|
||||
indexing_runner.batch_add_segments(document_segments, dataset)
|
||||
db.session.commit()
|
||||
redis_client.setex(indexing_cache_key, 600, 'completed')
|
||||
end_at = time.perf_counter()
|
||||
logging.info(click.style('Segment batch created job: {} latency: {}'.format(job_id, end_at - start_at), fg='green'))
|
||||
except Exception as e:
|
||||
logging.exception("Segments batch created index failed:{}".format(str(e)))
|
||||
redis_client.setex(indexing_cache_key, 600, 'error')
|
||||
@ -0,0 +1,58 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
import click
|
||||
from celery import shared_task
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from core.index.index import IndexBuilder
|
||||
from extensions.ext_database import db
|
||||
from extensions.ext_redis import redis_client
|
||||
from models.dataset import DocumentSegment, Dataset, Document
|
||||
|
||||
|
||||
@shared_task(queue='dataset')
|
||||
def delete_segment_from_index_task(segment_id: str, index_node_id: str, dataset_id: str, document_id: str):
|
||||
"""
|
||||
Async Remove segment from index
|
||||
:param segment_id:
|
||||
:param index_node_id:
|
||||
:param dataset_id:
|
||||
:param document_id:
|
||||
|
||||
Usage: delete_segment_from_index_task.delay(segment_id)
|
||||
"""
|
||||
logging.info(click.style('Start delete segment from index: {}'.format(segment_id), fg='green'))
|
||||
start_at = time.perf_counter()
|
||||
indexing_cache_key = 'segment_{}_delete_indexing'.format(segment_id)
|
||||
try:
|
||||
dataset = db.session.query(Dataset).filter(Dataset.id == dataset_id).first()
|
||||
if not dataset:
|
||||
logging.info(click.style('Segment {} has no dataset, pass.'.format(segment_id), fg='cyan'))
|
||||
return
|
||||
|
||||
dataset_document = db.session.query(Document).filter(Document.id == document_id).first()
|
||||
if not dataset_document:
|
||||
logging.info(click.style('Segment {} has no document, pass.'.format(segment_id), fg='cyan'))
|
||||
return
|
||||
|
||||
if not dataset_document.enabled or dataset_document.archived or dataset_document.indexing_status != 'completed':
|
||||
logging.info(click.style('Segment {} document status is invalid, pass.'.format(segment_id), fg='cyan'))
|
||||
return
|
||||
|
||||
vector_index = IndexBuilder.get_index(dataset, 'high_quality')
|
||||
kw_index = IndexBuilder.get_index(dataset, 'economy')
|
||||
|
||||
# delete from vector index
|
||||
if vector_index:
|
||||
vector_index.delete_by_ids([index_node_id])
|
||||
|
||||
# delete from keyword index
|
||||
kw_index.delete_by_ids([index_node_id])
|
||||
|
||||
end_at = time.perf_counter()
|
||||
logging.info(click.style('Segment deleted from index: {} latency: {}'.format(segment_id, end_at - start_at), fg='green'))
|
||||
except Exception:
|
||||
logging.exception("delete segment from index failed")
|
||||
finally:
|
||||
redis_client.delete(indexing_cache_key)
|
||||
@ -1,9 +1,9 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon">
|
||||
<g id="Icon_2">
|
||||
<path d="M6 6.5C6.27614 6.5 6.5 6.27614 6.5 6C6.5 5.72386 6.27614 5.5 6 5.5C5.72386 5.5 5.5 5.72386 5.5 6C5.5 6.27614 5.72386 6.5 6 6.5Z" stroke="#344054" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.5 6.5C9.77614 6.5 10 6.27614 10 6C10 5.72386 9.77614 5.5 9.5 5.5C9.22386 5.5 9 5.72386 9 6C9 6.27614 9.22386 6.5 9.5 6.5Z" stroke="#344054" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.5 6.5C2.77614 6.5 3 6.27614 3 6C3 5.72386 2.77614 5.5 2.5 5.5C2.22386 5.5 2 5.72386 2 6C2 6.27614 2.22386 6.5 2.5 6.5Z" stroke="#344054" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.00008 8.66634C8.36827 8.66634 8.66675 8.36786 8.66675 7.99967C8.66675 7.63148 8.36827 7.33301 8.00008 7.33301C7.63189 7.33301 7.33341 7.63148 7.33341 7.99967C7.33341 8.36786 7.63189 8.66634 8.00008 8.66634Z" stroke="#344054" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.6667 8.66634C13.0349 8.66634 13.3334 8.36786 13.3334 7.99967C13.3334 7.63148 13.0349 7.33301 12.6667 7.33301C12.2986 7.33301 12.0001 7.63148 12.0001 7.99967C12.0001 8.36786 12.2986 8.66634 12.6667 8.66634Z" stroke="#344054" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3.33341 8.66634C3.7016 8.66634 4.00008 8.36786 4.00008 7.99967C4.00008 7.63148 3.7016 7.33301 3.33341 7.33301C2.96522 7.33301 2.66675 7.63148 2.66675 7.99967C2.66675 8.36786 2.96522 8.66634 3.33341 8.66634Z" stroke="#344054" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 803 B After Width: | Height: | Size: 1.0 KiB |
@ -0,0 +1,38 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import Popover from '@/app/components/base/popover'
|
||||
|
||||
export type ILanguageSelectProps = {
|
||||
currentLanguage: string
|
||||
onSelect: (language: string) => void
|
||||
}
|
||||
|
||||
const LanguageSelect: FC<ILanguageSelectProps> = ({
|
||||
currentLanguage,
|
||||
onSelect,
|
||||
}) => {
|
||||
return (
|
||||
<Popover
|
||||
manualClose
|
||||
trigger='click'
|
||||
htmlContent={
|
||||
<div className='w-full py-1'>
|
||||
<div className='py-2 px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer text-gray-700 text-sm' onClick={() => onSelect('English')}>English</div>
|
||||
<div className='py-2 px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer text-gray-700 text-sm' onClick={() => onSelect('Chinese')}>简体中文</div>
|
||||
</div>
|
||||
}
|
||||
btnElement={
|
||||
<div className='inline-flex items-center'>
|
||||
<span className='pr-[2px] text-xs leading-[18px] font-medium'>{currentLanguage === 'English' ? 'English' : '简体中文'}</span>
|
||||
<ChevronDown className='w-3 h-3 opacity-60' />
|
||||
</div>
|
||||
}
|
||||
btnClassName={open => cn('!border-0 !px-0 !py-0 !bg-inherit !hover:bg-inherit', open ? 'text-blue-600' : 'text-gray-500')}
|
||||
className='!w-[120px] h-fit !z-20 !translate-x-0 !left-[-16px]'
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(LanguageSelect)
|
||||
@ -0,0 +1,108 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
useCSVDownloader,
|
||||
} from 'react-papaparse'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { Download02 as DownloadIcon } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { DocForm } from '@/models/datasets'
|
||||
import I18n from '@/context/i18n'
|
||||
|
||||
const CSV_TEMPLATE_QA_EN = [
|
||||
['question', 'answer'],
|
||||
['question1', 'answer1'],
|
||||
['question2', 'answer2'],
|
||||
]
|
||||
const CSV_TEMPLATE_QA_CN = [
|
||||
['问题', '答案'],
|
||||
['问题 1', '答案 1'],
|
||||
['问题 2', '答案 2'],
|
||||
]
|
||||
const CSV_TEMPLATE_EN = [
|
||||
['segment content'],
|
||||
['content1'],
|
||||
['content2'],
|
||||
]
|
||||
const CSV_TEMPLATE_CN = [
|
||||
['分段内容'],
|
||||
['内容 1'],
|
||||
['内容 2'],
|
||||
]
|
||||
|
||||
const CSVDownload: FC<{ docForm: DocForm }> = ({ docForm }) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const { CSVDownloader, Type } = useCSVDownloader()
|
||||
|
||||
const getTemplate = () => {
|
||||
if (locale === 'en') {
|
||||
if (docForm === DocForm.QA)
|
||||
return CSV_TEMPLATE_QA_EN
|
||||
return CSV_TEMPLATE_EN
|
||||
}
|
||||
if (docForm === DocForm.QA)
|
||||
return CSV_TEMPLATE_QA_CN
|
||||
return CSV_TEMPLATE_CN
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mt-6'>
|
||||
<div className='text-sm text-gray-900 font-medium'>{t('share.generation.csvStructureTitle')}</div>
|
||||
<div className='mt-2 max-h-[500px] overflow-auto'>
|
||||
{docForm === DocForm.QA && (
|
||||
<table className='table-fixed w-full border-separate border-spacing-0 border border-gray-200 rounded-lg text-xs'>
|
||||
<thead className='text-gray-500'>
|
||||
<tr>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-gray-200'>{t('datasetDocuments.list.batchModal.question')}</td>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-gray-200'>{t('datasetDocuments.list.batchModal.answer')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className='text-gray-700'>
|
||||
<tr>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-gray-100 text-[13px]'>{t('datasetDocuments.list.batchModal.question')} 1</td>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-gray-100 text-[13px]'>{t('datasetDocuments.list.batchModal.answer')} 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='h-9 pl-3 pr-2 text-[13px]'>{t('datasetDocuments.list.batchModal.question')} 2</td>
|
||||
<td className='h-9 pl-3 pr-2 text-[13px]'>{t('datasetDocuments.list.batchModal.answer')} 2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
{docForm === DocForm.TEXT && (
|
||||
<table className='table-fixed w-full border-separate border-spacing-0 border border-gray-200 rounded-lg text-xs'>
|
||||
<thead className='text-gray-500'>
|
||||
<tr>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-gray-200'>{t('datasetDocuments.list.batchModal.contentTitle')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className='text-gray-700'>
|
||||
<tr>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-gray-100 text-[13px]'>{t('datasetDocuments.list.batchModal.content')} 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='h-9 pl-3 pr-2 text-[13px]'>{t('datasetDocuments.list.batchModal.content')} 2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
<CSVDownloader
|
||||
className="block mt-2 cursor-pointer"
|
||||
type={Type.Link}
|
||||
filename={'template'}
|
||||
bom={true}
|
||||
data={getTemplate()}
|
||||
>
|
||||
<div className='flex items-center h-[18px] space-x-1 text-[#155EEF] text-xs font-medium'>
|
||||
<DownloadIcon className='w-3 h-3 mr-1' />
|
||||
{t('datasetDocuments.list.batchModal.template')}
|
||||
</div>
|
||||
</CSVDownloader>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(CSVDownload)
|
||||
@ -0,0 +1,126 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { Trash03 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
export type Props = {
|
||||
file: File | undefined
|
||||
updateFile: (file?: File) => void
|
||||
}
|
||||
|
||||
const CSVUploader: FC<Props> = ({
|
||||
file,
|
||||
updateFile,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const dropRef = useRef<HTMLDivElement>(null)
|
||||
const dragRef = useRef<HTMLDivElement>(null)
|
||||
const fileUploader = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target !== dragRef.current && setDragging(true)
|
||||
}
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target === dragRef.current && setDragging(false)
|
||||
}
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setDragging(false)
|
||||
if (!e.dataTransfer)
|
||||
return
|
||||
const files = [...e.dataTransfer.files]
|
||||
if (files.length > 1) {
|
||||
notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.validation.count') })
|
||||
return
|
||||
}
|
||||
updateFile(files[0])
|
||||
}
|
||||
const selectHandle = () => {
|
||||
if (fileUploader.current)
|
||||
fileUploader.current.click()
|
||||
}
|
||||
const removeFile = () => {
|
||||
if (fileUploader.current)
|
||||
fileUploader.current.value = ''
|
||||
updateFile()
|
||||
}
|
||||
const fileChangeHandle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const currentFile = e.target.files?.[0]
|
||||
updateFile(currentFile)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dropRef.current?.addEventListener('dragenter', handleDragEnter)
|
||||
dropRef.current?.addEventListener('dragover', handleDragOver)
|
||||
dropRef.current?.addEventListener('dragleave', handleDragLeave)
|
||||
dropRef.current?.addEventListener('drop', handleDrop)
|
||||
return () => {
|
||||
dropRef.current?.removeEventListener('dragenter', handleDragEnter)
|
||||
dropRef.current?.removeEventListener('dragover', handleDragOver)
|
||||
dropRef.current?.removeEventListener('dragleave', handleDragLeave)
|
||||
dropRef.current?.removeEventListener('drop', handleDrop)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='mt-6'>
|
||||
<input
|
||||
ref={fileUploader}
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
id="fileUploader"
|
||||
accept='.csv'
|
||||
onChange={fileChangeHandle}
|
||||
/>
|
||||
<div ref={dropRef}>
|
||||
{!file && (
|
||||
<div className={cn('flex items-center h-20 rounded-xl bg-gray-50 border border-dashed border-gray-200 text-sm font-normal', dragging && 'bg-[#F5F8FF] border border-[#B2CCFF]')}>
|
||||
<div className='w-full flex items-center justify-center space-x-2'>
|
||||
<CSVIcon className="shrink-0" />
|
||||
<div className='text-gray-500'>
|
||||
{t('datasetDocuments.list.batchModal.csvUploadTitle')}
|
||||
<span className='text-primary-400 cursor-pointer' onClick={selectHandle}>{t('datasetDocuments.list.batchModal.browse')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{dragging && <div ref={dragRef} className='absolute w-full h-full top-0 left-0'/>}
|
||||
</div>
|
||||
)}
|
||||
{file && (
|
||||
<div className={cn('flex items-center h-20 px-6 rounded-xl bg-gray-50 border border-gray-200 text-sm font-normal group', 'hover:bg-[#F5F8FF] hover:border-[#B2CCFF]')}>
|
||||
<CSVIcon className="shrink-0" />
|
||||
<div className='flex ml-2 w-0 grow'>
|
||||
<span className='max-w-[calc(100%_-_30px)] text-ellipsis whitespace-nowrap overflow-hidden text-gray-800'>{file.name.replace(/.csv$/, '')}</span>
|
||||
<span className='shrink-0 text-gray-500'>.csv</span>
|
||||
</div>
|
||||
<div className='hidden group-hover:flex items-center'>
|
||||
<Button className='!h-8 !px-3 !py-[6px] bg-white !text-[13px] !leading-[18px] text-gray-700' onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.change')}</Button>
|
||||
<div className='mx-2 w-px h-4 bg-gray-200' />
|
||||
<div className='p-2 cursor-pointer' onClick={removeFile}>
|
||||
<Trash03 className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(CSVUploader)
|
||||
@ -0,0 +1,65 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import CSVUploader from './csv-uploader'
|
||||
import CSVDownloader from './csv-downloader'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import type { DocForm } from '@/models/datasets'
|
||||
|
||||
export type IBatchModalProps = {
|
||||
isShow: boolean
|
||||
docForm: DocForm
|
||||
onCancel: () => void
|
||||
onConfirm: (file: File) => void
|
||||
}
|
||||
|
||||
const BatchModal: FC<IBatchModalProps> = ({
|
||||
isShow,
|
||||
docForm,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [currentCSV, setCurrentCSV] = useState<File>()
|
||||
const handleFile = (file?: File) => setCurrentCSV(file)
|
||||
|
||||
const handleSend = () => {
|
||||
if (!currentCSV)
|
||||
return
|
||||
onCancel()
|
||||
onConfirm(currentCSV)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isShow)
|
||||
setCurrentCSV(undefined)
|
||||
}, [isShow])
|
||||
|
||||
return (
|
||||
<Modal isShow={isShow} onClose={() => {}} className='px-8 py-6 !max-w-[520px] !rounded-xl'>
|
||||
<div className='relative pb-1 text-xl font-medium leading-[30px] text-gray-900'>{t('datasetDocuments.list.batchModal.title')}</div>
|
||||
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onCancel}>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
<CSVUploader
|
||||
file={currentCSV}
|
||||
updateFile={handleFile}
|
||||
/>
|
||||
<CSVDownloader
|
||||
docForm={docForm}
|
||||
/>
|
||||
<div className='mt-[28px] pt-6 flex justify-end'>
|
||||
<Button className='mr-2 text-gray-700 text-sm font-medium' onClick={onCancel}>
|
||||
{t('datasetDocuments.list.batchModal.cancel')}
|
||||
</Button>
|
||||
<Button className='text-sm font-medium' type="primary" onClick={handleSend} disabled={!currentCSV}>
|
||||
{t('datasetDocuments.list.batchModal.run')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(BatchModal)
|
||||
@ -0,0 +1,84 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import { FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import { Loading02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { AlertCircle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import Popover from '@/app/components/base/popover'
|
||||
|
||||
export type ISegmentAddProps = {
|
||||
importStatus: ProcessStatus | string | undefined
|
||||
clearProcessStatus: () => void
|
||||
showNewSegmentModal: () => void
|
||||
showBatchModal: () => void
|
||||
}
|
||||
|
||||
export enum ProcessStatus {
|
||||
WAITING = 'waiting',
|
||||
PROCESSING = 'processing',
|
||||
COMPLETED = 'completed',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
const SegmentAdd: FC<ISegmentAddProps> = ({
|
||||
importStatus,
|
||||
clearProcessStatus,
|
||||
showNewSegmentModal,
|
||||
showBatchModal,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (importStatus) {
|
||||
return (
|
||||
<>
|
||||
{(importStatus === ProcessStatus.WAITING || importStatus === ProcessStatus.PROCESSING) && (
|
||||
<div className='relative overflow-hidden inline-flex items-center mr-2 px-3 py-[6px] text-blue-700 bg-[#F5F8FF] rounded-lg border border-black/5'>
|
||||
{importStatus === ProcessStatus.WAITING && <div className='absolute left-0 top-0 w-3/12 h-full bg-[#D1E0FF] z-0'/>}
|
||||
{importStatus === ProcessStatus.PROCESSING && <div className='absolute left-0 top-0 w-2/3 h-full bg-[#D1E0FF] z-0'/>}
|
||||
<Loading02 className='animate-spin mr-2 w-4 h-4' />
|
||||
<span className='font-medium text-[13px] leading-[18px] z-10'>{t('datasetDocuments.list.batchModal.processing')}</span>
|
||||
</div>
|
||||
)}
|
||||
{importStatus === ProcessStatus.COMPLETED && (
|
||||
<div className='inline-flex items-center mr-2 px-3 py-[6px] text-gray-700 bg-[#F6FEF9] rounded-lg border border-black/5'>
|
||||
<CheckCircle className='mr-2 w-4 h-4 text-[#039855]' />
|
||||
<span className='font-medium text-[13px] leading-[18px]'>{t('datasetDocuments.list.batchModal.completed')}</span>
|
||||
<span className='pl-2 font-medium text-[13px] leading-[18px] text-[#155EEF] cursor-pointer' onClick={clearProcessStatus}>{t('datasetDocuments.list.batchModal.ok')}</span>
|
||||
</div>
|
||||
)}
|
||||
{importStatus === ProcessStatus.ERROR && (
|
||||
<div className='inline-flex items-center mr-2 px-3 py-[6px] text-red-600 bg-red-100 rounded-lg border border-black/5'>
|
||||
<AlertCircle className='mr-2 w-4 h-4 text-[#D92D20]' />
|
||||
<span className='font-medium text-[13px] leading-[18px]'>{t('datasetDocuments.list.batchModal.error')}</span>
|
||||
<span className='pl-2 font-medium text-[13px] leading-[18px] text-[#155EEF] cursor-pointer' onClick={clearProcessStatus}>{t('datasetDocuments.list.batchModal.ok')}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover
|
||||
manualClose
|
||||
trigger='click'
|
||||
htmlContent={
|
||||
<div className='w-full py-1'>
|
||||
<div className='py-2 px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer text-gray-700 text-sm' onClick={showNewSegmentModal}>{t('datasetDocuments.list.action.add')}</div>
|
||||
<div className='py-2 px-3 mx-1 flex items-center gap-2 hover:bg-gray-100 rounded-lg cursor-pointer text-gray-700 text-sm' onClick={showBatchModal}>{t('datasetDocuments.list.action.batchAdd')}</div>
|
||||
</div>
|
||||
}
|
||||
btnElement={
|
||||
<div className='inline-flex items-center'>
|
||||
<FilePlus02 className='w-4 h-4 text-gray-700' />
|
||||
<span className='pl-1'>{t('datasetDocuments.list.action.addButton')}</span>
|
||||
</div>
|
||||
}
|
||||
btnClassName={open => cn('mr-2 !py-[6px] !text-[13px] !leading-[18px] hover:bg-gray-50 border border-gray-200 hover:border-gray-300 hover:shadow-[0_1px_2px_rgba(16,24,40,0.05)]', open ? '!bg-gray-100 !shadow-none' : '!bg-transparent')}
|
||||
className='!w-[132px] h-fit !z-20 !translate-x-0 !left-0'
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(SegmentAdd)
|
||||
Loading…
Reference in New Issue