mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-11 20:09:06 +08:00
Fix: disable operations of dataset when embedding unavailable (#1055)
Co-authored-by: jyong <jyong@dify.ai>
This commit is contained in:
parent
8b8e510bfe
commit
c67f345d0e
@ -148,14 +148,28 @@ class DatasetApi(Resource):
|
|||||||
dataset = DatasetService.get_dataset(dataset_id_str)
|
dataset = DatasetService.get_dataset(dataset_id_str)
|
||||||
if dataset is None:
|
if dataset is None:
|
||||||
raise NotFound("Dataset not found.")
|
raise NotFound("Dataset not found.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
DatasetService.check_dataset_permission(
|
DatasetService.check_dataset_permission(
|
||||||
dataset, current_user)
|
dataset, current_user)
|
||||||
except services.errors.account.NoPermissionError as e:
|
except services.errors.account.NoPermissionError as e:
|
||||||
raise Forbidden(str(e))
|
raise Forbidden(str(e))
|
||||||
|
data = marshal(dataset, dataset_detail_fields)
|
||||||
return marshal(dataset, dataset_detail_fields), 200
|
# check embedding setting
|
||||||
|
provider_service = ProviderService()
|
||||||
|
# get valid model list
|
||||||
|
valid_model_list = provider_service.get_valid_model_list(current_user.current_tenant_id, ModelType.EMBEDDINGS.value)
|
||||||
|
model_names = []
|
||||||
|
for valid_model in valid_model_list:
|
||||||
|
model_names.append(f"{valid_model['model_name']}:{valid_model['model_provider']['provider_name']}")
|
||||||
|
if data['indexing_technique'] == 'high_quality':
|
||||||
|
item_model = f"{data['embedding_model']}:{data['embedding_model_provider']}"
|
||||||
|
if item_model in model_names:
|
||||||
|
data['embedding_available'] = True
|
||||||
|
else:
|
||||||
|
data['embedding_available'] = False
|
||||||
|
else:
|
||||||
|
data['embedding_available'] = True
|
||||||
|
return data, 200
|
||||||
|
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -137,28 +137,31 @@ class DatasetService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_dataset(dataset_id, data, user):
|
def update_dataset(dataset_id, data, user):
|
||||||
|
filtered_data = {k: v for k, v in data.items() if v is not None or k == 'description'}
|
||||||
dataset = DatasetService.get_dataset(dataset_id)
|
dataset = DatasetService.get_dataset(dataset_id)
|
||||||
DatasetService.check_dataset_permission(dataset, user)
|
DatasetService.check_dataset_permission(dataset, user)
|
||||||
|
action = None
|
||||||
if dataset.indexing_technique != data['indexing_technique']:
|
if dataset.indexing_technique != data['indexing_technique']:
|
||||||
# if update indexing_technique
|
# if update indexing_technique
|
||||||
if data['indexing_technique'] == 'economy':
|
if data['indexing_technique'] == 'economy':
|
||||||
deal_dataset_vector_index_task.delay(dataset_id, 'remove')
|
action = 'remove'
|
||||||
|
filtered_data['embedding_model'] = None
|
||||||
|
filtered_data['embedding_model_provider'] = None
|
||||||
elif data['indexing_technique'] == 'high_quality':
|
elif data['indexing_technique'] == 'high_quality':
|
||||||
# check embedding model setting
|
action = 'add'
|
||||||
|
# get embedding model setting
|
||||||
try:
|
try:
|
||||||
ModelFactory.get_embedding_model(
|
embedding_model = ModelFactory.get_embedding_model(
|
||||||
tenant_id=current_user.current_tenant_id,
|
tenant_id=current_user.current_tenant_id
|
||||||
model_provider_name=dataset.embedding_model_provider,
|
|
||||||
model_name=dataset.embedding_model
|
|
||||||
)
|
)
|
||||||
|
filtered_data['embedding_model'] = embedding_model.name
|
||||||
|
filtered_data['embedding_model_provider'] = embedding_model.model_provider.provider_name
|
||||||
except LLMBadRequestError:
|
except LLMBadRequestError:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"No Embedding Model available. Please configure a valid provider "
|
f"No Embedding Model available. Please configure a valid provider "
|
||||||
f"in the Settings -> Model Provider.")
|
f"in the Settings -> Model Provider.")
|
||||||
except ProviderTokenNotInitError as ex:
|
except ProviderTokenNotInitError as ex:
|
||||||
raise ValueError(ex.description)
|
raise ValueError(ex.description)
|
||||||
deal_dataset_vector_index_task.delay(dataset_id, 'add')
|
|
||||||
filtered_data = {k: v for k, v in data.items() if v is not None or k == 'description'}
|
|
||||||
|
|
||||||
filtered_data['updated_by'] = user.id
|
filtered_data['updated_by'] = user.id
|
||||||
filtered_data['updated_at'] = datetime.datetime.now()
|
filtered_data['updated_at'] = datetime.datetime.now()
|
||||||
@ -166,7 +169,8 @@ class DatasetService:
|
|||||||
dataset.query.filter_by(id=dataset_id).update(filtered_data)
|
dataset.query.filter_by(id=dataset_id).update(filtered_data)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
if action:
|
||||||
|
deal_dataset_vector_index_task.delay(dataset_id, action)
|
||||||
return dataset
|
return dataset
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -43,7 +43,7 @@ const CardItem: FC<ICardItemProps> = ({
|
|||||||
selector={`unavailable-tag-${config.id}`}
|
selector={`unavailable-tag-${config.id}`}
|
||||||
htmlContent={t('dataset.unavailableTip')}
|
htmlContent={t('dataset.unavailableTip')}
|
||||||
>
|
>
|
||||||
<span className='shrink-0 px-1 border boder-gray-200 rounded-md text-gray-500 text-xs font-normal leading-[18px]'>{t('dataset.unavailable')}</span>
|
<span className='shrink-0 inline-flex whitespace-nowrap px-1 border boder-gray-200 rounded-md text-gray-500 text-xs font-normal leading-[18px]'>{t('dataset.unavailable')}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,7 +15,7 @@ type IInfiniteVirtualListProps = {
|
|||||||
onChangeSwitch: (segId: string, enabled: boolean) => Promise<void>
|
onChangeSwitch: (segId: string, enabled: boolean) => Promise<void>
|
||||||
onDelete: (segId: string) => Promise<void>
|
onDelete: (segId: string) => Promise<void>
|
||||||
archived?: boolean
|
archived?: boolean
|
||||||
|
embeddingAvailable: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const InfiniteVirtualList: FC<IInfiniteVirtualListProps> = ({
|
const InfiniteVirtualList: FC<IInfiniteVirtualListProps> = ({
|
||||||
@ -27,6 +27,7 @@ const InfiniteVirtualList: FC<IInfiniteVirtualListProps> = ({
|
|||||||
onChangeSwitch,
|
onChangeSwitch,
|
||||||
onDelete,
|
onDelete,
|
||||||
archived,
|
archived,
|
||||||
|
embeddingAvailable,
|
||||||
}) => {
|
}) => {
|
||||||
// If there are more items to be loaded then add an extra row to hold a loading indicator.
|
// If there are more items to be loaded then add an extra row to hold a loading indicator.
|
||||||
const itemCount = hasNextPage ? items.length + 1 : items.length
|
const itemCount = hasNextPage ? items.length + 1 : items.length
|
||||||
@ -45,7 +46,7 @@ const InfiniteVirtualList: FC<IInfiniteVirtualListProps> = ({
|
|||||||
content = (
|
content = (
|
||||||
<>
|
<>
|
||||||
{[1, 2, 3].map(v => (
|
{[1, 2, 3].map(v => (
|
||||||
<SegmentCard loading={true} detail={{ position: v } as any} />
|
<SegmentCard key={v} loading={true} detail={{ position: v } as any} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
@ -60,6 +61,7 @@ const InfiniteVirtualList: FC<IInfiniteVirtualListProps> = ({
|
|||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
loading={false}
|
loading={false}
|
||||||
archived={archived}
|
archived={archived}
|
||||||
|
embeddingAvailable={embeddingAvailable}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -43,6 +43,7 @@ type ISegmentCardProps = {
|
|||||||
scene?: UsageScene
|
scene?: UsageScene
|
||||||
className?: string
|
className?: string
|
||||||
archived?: boolean
|
archived?: boolean
|
||||||
|
embeddingAvailable: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const SegmentCard: FC<ISegmentCardProps> = ({
|
const SegmentCard: FC<ISegmentCardProps> = ({
|
||||||
@ -55,6 +56,7 @@ const SegmentCard: FC<ISegmentCardProps> = ({
|
|||||||
scene = 'doc',
|
scene = 'doc',
|
||||||
className = '',
|
className = '',
|
||||||
archived,
|
archived,
|
||||||
|
embeddingAvailable,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const {
|
const {
|
||||||
@ -115,24 +117,26 @@ const SegmentCard: FC<ISegmentCardProps> = ({
|
|||||||
: (
|
: (
|
||||||
<>
|
<>
|
||||||
<StatusItem status={enabled ? 'enabled' : 'disabled'} reverse textCls="text-gray-500 text-xs" />
|
<StatusItem status={enabled ? 'enabled' : 'disabled'} reverse textCls="text-gray-500 text-xs" />
|
||||||
<div className="hidden group-hover:inline-flex items-center">
|
{embeddingAvailable && (
|
||||||
<Divider type="vertical" className="!h-2" />
|
<div className="hidden group-hover:inline-flex items-center">
|
||||||
<div
|
<Divider type="vertical" className="!h-2" />
|
||||||
onClick={(e: React.MouseEvent<HTMLDivElement, MouseEvent>) =>
|
<div
|
||||||
e.stopPropagation()
|
onClick={(e: React.MouseEvent<HTMLDivElement, MouseEvent>) =>
|
||||||
}
|
e.stopPropagation()
|
||||||
className="inline-flex items-center"
|
}
|
||||||
>
|
className="inline-flex items-center"
|
||||||
<Switch
|
>
|
||||||
size='md'
|
<Switch
|
||||||
disabled={archived}
|
size='md'
|
||||||
defaultValue={enabled}
|
disabled={archived}
|
||||||
onChange={async (val) => {
|
defaultValue={enabled}
|
||||||
await onChangeSwitch?.(id, val)
|
onChange={async (val) => {
|
||||||
}}
|
await onChangeSwitch?.(id, val)
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -173,7 +177,7 @@ const SegmentCard: FC<ISegmentCardProps> = ({
|
|||||||
<div className={cn(s.commonIcon, s.bezierCurveIcon)} />
|
<div className={cn(s.commonIcon, s.bezierCurveIcon)} />
|
||||||
<div className={s.segDataText}>{index_node_hash}</div>
|
<div className={s.segDataText}>{index_node_hash}</div>
|
||||||
</div>
|
</div>
|
||||||
{!archived && (
|
{!archived && embeddingAvailable && (
|
||||||
<div className='shrink-0 w-6 h-6 flex items-center justify-center rounded-md hover:bg-red-100 hover:text-red-600 cursor-pointer group/delete' onClick={(e) => {
|
<div className='shrink-0 w-6 h-6 flex items-center justify-center rounded-md hover:bg-red-100 hover:text-red-600 cursor-pointer group/delete' onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setShowModal(true)
|
setShowModal(true)
|
||||||
|
@ -46,6 +46,7 @@ export const SegmentIndexTag: FC<{ positionId: string | number; className?: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ISegmentDetailProps = {
|
type ISegmentDetailProps = {
|
||||||
|
embeddingAvailable: boolean
|
||||||
segInfo?: Partial<SegmentDetailModel> & { id: string }
|
segInfo?: Partial<SegmentDetailModel> & { id: string }
|
||||||
onChangeSwitch?: (segId: string, enabled: boolean) => Promise<void>
|
onChangeSwitch?: (segId: string, enabled: boolean) => Promise<void>
|
||||||
onUpdate: (segmentId: string, q: string, a: string, k: string[]) => void
|
onUpdate: (segmentId: string, q: string, a: string, k: string[]) => void
|
||||||
@ -56,6 +57,7 @@ type ISegmentDetailProps = {
|
|||||||
* Show all the contents of the segment
|
* Show all the contents of the segment
|
||||||
*/
|
*/
|
||||||
const SegmentDetailComponent: FC<ISegmentDetailProps> = ({
|
const SegmentDetailComponent: FC<ISegmentDetailProps> = ({
|
||||||
|
embeddingAvailable,
|
||||||
segInfo,
|
segInfo,
|
||||||
archived,
|
archived,
|
||||||
onChangeSwitch,
|
onChangeSwitch,
|
||||||
@ -146,7 +148,7 @@ const SegmentDetailComponent: FC<ISegmentDetailProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!isEditing && !archived && (
|
{!isEditing && !archived && embeddingAvailable && (
|
||||||
<>
|
<>
|
||||||
<div className='group relative flex justify-center items-center w-6 h-6 hover:bg-gray-100 rounded-md cursor-pointer'>
|
<div className='group relative flex justify-center items-center w-6 h-6 hover:bg-gray-100 rounded-md cursor-pointer'>
|
||||||
<div className={cn(s.editTip, 'hidden items-center absolute -top-10 px-3 h-[34px] bg-white rounded-lg whitespace-nowrap text-xs font-semibold text-gray-700 group-hover:flex')}>{t('common.operation.edit')}</div>
|
<div className={cn(s.editTip, 'hidden items-center absolute -top-10 px-3 h-[34px] bg-white rounded-lg whitespace-nowrap text-xs font-semibold text-gray-700 group-hover:flex')}>{t('common.operation.edit')}</div>
|
||||||
@ -183,15 +185,19 @@ const SegmentDetailComponent: FC<ISegmentDetailProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className='flex items-center'>
|
<div className='flex items-center'>
|
||||||
<StatusItem status={segInfo?.enabled ? 'enabled' : 'disabled'} reverse textCls='text-gray-500 text-xs' />
|
<StatusItem status={segInfo?.enabled ? 'enabled' : 'disabled'} reverse textCls='text-gray-500 text-xs' />
|
||||||
<Divider type='vertical' className='!h-2' />
|
{embeddingAvailable && (
|
||||||
<Switch
|
<>
|
||||||
size='md'
|
<Divider type='vertical' className='!h-2' />
|
||||||
defaultValue={segInfo?.enabled}
|
<Switch
|
||||||
onChange={async (val) => {
|
size='md'
|
||||||
await onChangeSwitch?.(segInfo?.id || '', val)
|
defaultValue={segInfo?.enabled}
|
||||||
}}
|
onChange={async (val) => {
|
||||||
disabled={archived}
|
await onChangeSwitch?.(segInfo?.id || '', val)
|
||||||
/>
|
}}
|
||||||
|
disabled={archived}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -209,6 +215,7 @@ export const splitArray = (arr: any[], size = 3) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ICompletedProps = {
|
type ICompletedProps = {
|
||||||
|
embeddingAvailable: boolean
|
||||||
showNewSegmentModal: boolean
|
showNewSegmentModal: boolean
|
||||||
onNewSegmentModalChange: (state: boolean) => void
|
onNewSegmentModalChange: (state: boolean) => void
|
||||||
importStatus: ProcessStatus | string | undefined
|
importStatus: ProcessStatus | string | undefined
|
||||||
@ -220,6 +227,7 @@ type ICompletedProps = {
|
|||||||
* Support search and filter
|
* Support search and filter
|
||||||
*/
|
*/
|
||||||
const Completed: FC<ICompletedProps> = ({
|
const Completed: FC<ICompletedProps> = ({
|
||||||
|
embeddingAvailable,
|
||||||
showNewSegmentModal,
|
showNewSegmentModal,
|
||||||
onNewSegmentModalChange,
|
onNewSegmentModalChange,
|
||||||
importStatus,
|
importStatus,
|
||||||
@ -384,6 +392,7 @@ const Completed: FC<ICompletedProps> = ({
|
|||||||
<Input showPrefix wrapperClassName='!w-52' className='!h-8' onChange={debounce(setSearchValue, 500)} />
|
<Input showPrefix wrapperClassName='!w-52' className='!h-8' onChange={debounce(setSearchValue, 500)} />
|
||||||
</div>
|
</div>
|
||||||
<InfiniteVirtualList
|
<InfiniteVirtualList
|
||||||
|
embeddingAvailable={embeddingAvailable}
|
||||||
hasNextPage={lastSegmentsRes?.has_more ?? true}
|
hasNextPage={lastSegmentsRes?.has_more ?? true}
|
||||||
isNextPageLoading={loading}
|
isNextPageLoading={loading}
|
||||||
items={allSegments}
|
items={allSegments}
|
||||||
@ -395,6 +404,7 @@ const Completed: FC<ICompletedProps> = ({
|
|||||||
/>
|
/>
|
||||||
<Modal isShow={currSegment.showModal} onClose={() => {}} className='!max-w-[640px] !overflow-visible'>
|
<Modal isShow={currSegment.showModal} onClose={() => {}} className='!max-w-[640px] !overflow-visible'>
|
||||||
<SegmentDetail
|
<SegmentDetail
|
||||||
|
embeddingAvailable={embeddingAvailable}
|
||||||
segInfo={currSegment.segInfo ?? { id: '' }}
|
segInfo={currSegment.segInfo ?? { id: '' }}
|
||||||
onChangeSwitch={onChangeSwitch}
|
onChangeSwitch={onChangeSwitch}
|
||||||
onUpdate={handleUpdateSegment}
|
onUpdate={handleUpdateSegment}
|
||||||
|
@ -22,6 +22,7 @@ import type { MetadataType } from '@/service/datasets'
|
|||||||
import { checkSegmentBatchImportProgress, fetchDocumentDetail, segmentBatchImport } from '@/service/datasets'
|
import { checkSegmentBatchImportProgress, fetchDocumentDetail, segmentBatchImport } from '@/service/datasets'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
import type { DocForm } from '@/models/datasets'
|
import type { DocForm } from '@/models/datasets'
|
||||||
|
import { useDatasetDetailContext } from '@/context/dataset-detail'
|
||||||
|
|
||||||
export const DocumentContext = createContext<{ datasetId?: string; documentId?: string; docForm: string }>({ docForm: '' })
|
export const DocumentContext = createContext<{ datasetId?: string; documentId?: string; docForm: string }>({ docForm: '' })
|
||||||
|
|
||||||
@ -50,6 +51,8 @@ const DocumentDetail: FC<Props> = ({ datasetId, documentId }) => {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { notify } = useContext(ToastContext)
|
const { notify } = useContext(ToastContext)
|
||||||
|
const { dataset } = useDatasetDetailContext()
|
||||||
|
const embeddingAvailable = !!dataset?.embedding_available
|
||||||
const [showMetadata, setShowMetadata] = useState(true)
|
const [showMetadata, setShowMetadata] = useState(true)
|
||||||
const [newSegmentModalVisible, setNewSegmentModalVisible] = useState(false)
|
const [newSegmentModalVisible, setNewSegmentModalVisible] = useState(false)
|
||||||
const [batchModalVisible, setBatchModalVisible] = useState(false)
|
const [batchModalVisible, setBatchModalVisible] = useState(false)
|
||||||
@ -128,7 +131,7 @@ const DocumentDetail: FC<Props> = ({ datasetId, documentId }) => {
|
|||||||
<Divider className='!h-4' type='vertical' />
|
<Divider className='!h-4' type='vertical' />
|
||||||
<DocumentTitle extension={documentDetail?.data_source_info?.upload_file?.extension} name={documentDetail?.name} />
|
<DocumentTitle extension={documentDetail?.data_source_info?.upload_file?.extension} name={documentDetail?.name} />
|
||||||
<StatusItem status={documentDetail?.display_status || 'available'} scene='detail' errorMessage={documentDetail?.error || ''} />
|
<StatusItem status={documentDetail?.display_status || 'available'} scene='detail' errorMessage={documentDetail?.error || ''} />
|
||||||
{documentDetail && !documentDetail.archived && (
|
{embeddingAvailable && documentDetail && !documentDetail.archived && (
|
||||||
<SegmentAdd
|
<SegmentAdd
|
||||||
importStatus={importStatus}
|
importStatus={importStatus}
|
||||||
clearProcessStatus={resetProcessStatus}
|
clearProcessStatus={resetProcessStatus}
|
||||||
@ -138,6 +141,7 @@ const DocumentDetail: FC<Props> = ({ datasetId, documentId }) => {
|
|||||||
)}
|
)}
|
||||||
<OperationAction
|
<OperationAction
|
||||||
scene='detail'
|
scene='detail'
|
||||||
|
embeddingAvailable={embeddingAvailable}
|
||||||
detail={{
|
detail={{
|
||||||
enabled: documentDetail?.enabled || false,
|
enabled: documentDetail?.enabled || false,
|
||||||
archived: documentDetail?.archived || false,
|
archived: documentDetail?.archived || false,
|
||||||
@ -161,6 +165,7 @@ const DocumentDetail: FC<Props> = ({ datasetId, documentId }) => {
|
|||||||
{embedding
|
{embedding
|
||||||
? <Embedding detail={documentDetail} detailUpdate={detailMutate} />
|
? <Embedding detail={documentDetail} detailUpdate={detailMutate} />
|
||||||
: <Completed
|
: <Completed
|
||||||
|
embeddingAvailable={embeddingAvailable}
|
||||||
showNewSegmentModal={newSegmentModalVisible}
|
showNewSegmentModal={newSegmentModalVisible}
|
||||||
onNewSegmentModalChange={setNewSegmentModalVisible}
|
onNewSegmentModalChange={setNewSegmentModalVisible}
|
||||||
importStatus={importStatus}
|
importStatus={importStatus}
|
||||||
|
@ -51,7 +51,7 @@ const NotionIcon = ({ className }: React.SVGProps<SVGElement>) => {
|
|||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
|
|
||||||
const EmptyElement: FC<{ onClick: () => void; type?: 'upload' | 'sync' }> = ({ onClick, type = 'upload' }) => {
|
const EmptyElement: FC<{ canAdd: boolean; onClick: () => void; type?: 'upload' | 'sync' }> = ({ canAdd = true, onClick, type = 'upload' }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
return <div className={s.emptyWrapper}>
|
return <div className={s.emptyWrapper}>
|
||||||
<div className={s.emptyElement}>
|
<div className={s.emptyElement}>
|
||||||
@ -62,7 +62,7 @@ const EmptyElement: FC<{ onClick: () => void; type?: 'upload' | 'sync' }> = ({ o
|
|||||||
<div className={s.emptyTip}>
|
<div className={s.emptyTip}>
|
||||||
{t(`datasetDocuments.list.empty.${type}.tip`)}
|
{t(`datasetDocuments.list.empty.${type}.tip`)}
|
||||||
</div>
|
</div>
|
||||||
{type === 'upload' && <Button onClick={onClick} className={s.addFileBtn}>
|
{type === 'upload' && canAdd && <Button onClick={onClick} className={s.addFileBtn}>
|
||||||
<PlusIcon className={s.plusIcon} />{t('datasetDocuments.list.addFile')}
|
<PlusIcon className={s.plusIcon} />{t('datasetDocuments.list.addFile')}
|
||||||
</Button>}
|
</Button>}
|
||||||
</div>
|
</div>
|
||||||
@ -84,6 +84,7 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
|
|||||||
const [notionPageSelectorModalVisible, setNotionPageSelectorModalVisible] = useState(false)
|
const [notionPageSelectorModalVisible, setNotionPageSelectorModalVisible] = useState(false)
|
||||||
const [timerCanRun, setTimerCanRun] = useState(true)
|
const [timerCanRun, setTimerCanRun] = useState(true)
|
||||||
const isDataSourceNotion = dataset?.data_source_type === DataSourceType.NOTION
|
const isDataSourceNotion = dataset?.data_source_type === DataSourceType.NOTION
|
||||||
|
const embeddingAvailable = !!dataset?.embedding_available
|
||||||
|
|
||||||
const query = useMemo(() => {
|
const query = useMemo(() => {
|
||||||
return { page: currPage + 1, limit, keyword: searchValue, fetch: isDataSourceNotion ? true : '' }
|
return { page: currPage + 1, limit, keyword: searchValue, fetch: isDataSourceNotion ? true : '' }
|
||||||
@ -205,20 +206,19 @@ const Documents: FC<IDocumentsProps> = ({ datasetId }) => {
|
|||||||
onChange={debounce(setSearchValue, 500)}
|
onChange={debounce(setSearchValue, 500)}
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
/>
|
/>
|
||||||
<Button type='primary' onClick={routeToDocCreate} className='!h-8 !text-[13px]'>
|
{embeddingAvailable && (
|
||||||
<PlusIcon className='h-4 w-4 mr-2 stroke-current' />
|
<Button type='primary' onClick={routeToDocCreate} className='!h-8 !text-[13px]'>
|
||||||
{
|
<PlusIcon className='h-4 w-4 mr-2 stroke-current' />
|
||||||
isDataSourceNotion
|
{isDataSourceNotion && t('datasetDocuments.list.addPages')}
|
||||||
? t('datasetDocuments.list.addPages')
|
{!isDataSourceNotion && t('datasetDocuments.list.addFile')}
|
||||||
: t('datasetDocuments.list.addFile')
|
</Button>
|
||||||
}
|
)}
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
{isLoading
|
{isLoading
|
||||||
? <Loading type='app' />
|
? <Loading type='app' />
|
||||||
: total > 0
|
: total > 0
|
||||||
? <List documents={documentsList || []} datasetId={datasetId} onUpdate={mutate} />
|
? <List embeddingAvailable={embeddingAvailable} documents={documentsList || []} datasetId={datasetId} onUpdate={mutate} />
|
||||||
: <EmptyElement onClick={routeToDocCreate} type={isDataSourceNotion ? 'sync' : 'upload'} />
|
: <EmptyElement canAdd={embeddingAvailable} onClick={routeToDocCreate} type={isDataSourceNotion ? 'sync' : 'upload'} />
|
||||||
}
|
}
|
||||||
{/* Show Pagination only if the total is more than the limit */}
|
{/* Show Pagination only if the total is more than the limit */}
|
||||||
{(total && total > limit)
|
{(total && total > limit)
|
||||||
|
@ -103,6 +103,7 @@ type OperationName = 'delete' | 'archive' | 'enable' | 'disable' | 'sync' | 'un_
|
|||||||
|
|
||||||
// operation action for list and detail
|
// operation action for list and detail
|
||||||
export const OperationAction: FC<{
|
export const OperationAction: FC<{
|
||||||
|
embeddingAvailable: boolean
|
||||||
detail: {
|
detail: {
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
archived: boolean
|
archived: boolean
|
||||||
@ -114,7 +115,7 @@ export const OperationAction: FC<{
|
|||||||
onUpdate: (operationName?: string) => void
|
onUpdate: (operationName?: string) => void
|
||||||
scene?: 'list' | 'detail'
|
scene?: 'list' | 'detail'
|
||||||
className?: string
|
className?: string
|
||||||
}> = ({ datasetId, detail, onUpdate, scene = 'list', className = '' }) => {
|
}> = ({ embeddingAvailable, datasetId, detail, onUpdate, scene = 'list', className = '' }) => {
|
||||||
const { id, enabled = false, archived = false, data_source_type } = detail || {}
|
const { id, enabled = false, archived = false, data_source_type } = detail || {}
|
||||||
const [showModal, setShowModal] = useState(false)
|
const [showModal, setShowModal] = useState(false)
|
||||||
const { notify } = useContext(ToastContext)
|
const { notify } = useContext(ToastContext)
|
||||||
@ -154,87 +155,94 @@ export const OperationAction: FC<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <div className='flex items-center' onClick={e => e.stopPropagation()}>
|
return <div className='flex items-center' onClick={e => e.stopPropagation()}>
|
||||||
{isListScene && <>
|
{isListScene && !embeddingAvailable && (
|
||||||
{archived
|
<Switch defaultValue={false} onChange={() => { }} disabled={true} size='md' />
|
||||||
? <Tooltip selector={`list-switch-${id}`} content={t('datasetDocuments.list.action.enableWarning') as string} className='!font-semibold'>
|
)}
|
||||||
<div>
|
{isListScene && embeddingAvailable && (
|
||||||
<Switch defaultValue={false} onChange={() => { }} disabled={true} size='md' />
|
<>
|
||||||
</div>
|
{archived
|
||||||
</Tooltip>
|
? <Tooltip selector={`list-switch-${id}`} content={t('datasetDocuments.list.action.enableWarning') as string} className='!font-semibold'>
|
||||||
: <Switch defaultValue={enabled} onChange={v => onOperate(v ? 'enable' : 'disable')} size='md' />
|
<div>
|
||||||
}
|
<Switch defaultValue={false} onChange={() => { }} disabled={true} size='md' />
|
||||||
<Divider className='!ml-4 !mr-2 !h-3' type='vertical' />
|
|
||||||
</>}
|
|
||||||
<Popover
|
|
||||||
htmlContent={
|
|
||||||
<div className='w-full py-1'>
|
|
||||||
{!isListScene && <>
|
|
||||||
<div className='flex justify-between items-center mx-4 pt-2'>
|
|
||||||
<span className={cn(s.actionName, 'font-medium')}>
|
|
||||||
{!archived && enabled ? t('datasetDocuments.list.index.enable') : t('datasetDocuments.list.index.disable')}
|
|
||||||
</span>
|
|
||||||
<Tooltip
|
|
||||||
selector={`detail-switch-${id}`}
|
|
||||||
content={t('datasetDocuments.list.action.enableWarning') as string}
|
|
||||||
className='!font-semibold'
|
|
||||||
disabled={!archived}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Switch
|
|
||||||
defaultValue={archived ? false : enabled}
|
|
||||||
onChange={v => !archived && onOperate(v ? 'enable' : 'disable')}
|
|
||||||
disabled={archived}
|
|
||||||
size='md'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
<div className='mx-4 pb-1 pt-0.5 text-xs text-gray-500'>
|
</Tooltip>
|
||||||
{!archived && enabled ? t('datasetDocuments.list.index.enableTip') : t('datasetDocuments.list.index.disableTip')}
|
: <Switch defaultValue={enabled} onChange={v => onOperate(v ? 'enable' : 'disable')} size='md' />
|
||||||
</div>
|
}
|
||||||
<Divider />
|
<Divider className='!ml-4 !mr-2 !h-3' type='vertical' />
|
||||||
</>}
|
</>
|
||||||
{!archived && (
|
)}
|
||||||
<>
|
{embeddingAvailable && (
|
||||||
<div className={s.actionItem} onClick={() => router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)}>
|
<Popover
|
||||||
<SettingsIcon />
|
htmlContent={
|
||||||
<span className={s.actionName}>{t('datasetDocuments.list.action.settings')}</span>
|
<div className='w-full py-1'>
|
||||||
|
{!isListScene && <>
|
||||||
|
<div className='flex justify-between items-center mx-4 pt-2'>
|
||||||
|
<span className={cn(s.actionName, 'font-medium')}>
|
||||||
|
{!archived && enabled ? t('datasetDocuments.list.index.enable') : t('datasetDocuments.list.index.disable')}
|
||||||
|
</span>
|
||||||
|
<Tooltip
|
||||||
|
selector={`detail-switch-${id}`}
|
||||||
|
content={t('datasetDocuments.list.action.enableWarning') as string}
|
||||||
|
className='!font-semibold'
|
||||||
|
disabled={!archived}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Switch
|
||||||
|
defaultValue={archived ? false : enabled}
|
||||||
|
onChange={v => !archived && onOperate(v ? 'enable' : 'disable')}
|
||||||
|
disabled={archived}
|
||||||
|
size='md'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{data_source_type === 'notion_import' && (
|
<div className='mx-4 pb-1 pt-0.5 text-xs text-gray-500'>
|
||||||
<div className={s.actionItem} onClick={() => onOperate('sync')}>
|
{!archived && enabled ? t('datasetDocuments.list.index.enableTip') : t('datasetDocuments.list.index.disableTip')}
|
||||||
<SyncIcon />
|
</div>
|
||||||
<span className={s.actionName}>{t('datasetDocuments.list.action.sync')}</span>
|
<Divider />
|
||||||
|
</>}
|
||||||
|
{!archived && (
|
||||||
|
<>
|
||||||
|
<div className={s.actionItem} onClick={() => router.push(`/datasets/${datasetId}/documents/${detail.id}/settings`)}>
|
||||||
|
<SettingsIcon />
|
||||||
|
<span className={s.actionName}>{t('datasetDocuments.list.action.settings')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
{data_source_type === 'notion_import' && (
|
||||||
<Divider className='my-1' />
|
<div className={s.actionItem} onClick={() => onOperate('sync')}>
|
||||||
</>
|
<SyncIcon />
|
||||||
)}
|
<span className={s.actionName}>{t('datasetDocuments.list.action.sync')}</span>
|
||||||
{!archived && <div className={s.actionItem} onClick={() => onOperate('archive')}>
|
</div>
|
||||||
<ArchiveIcon />
|
)}
|
||||||
<span className={s.actionName}>{t('datasetDocuments.list.action.archive')}</span>
|
<Divider className='my-1' />
|
||||||
</div>}
|
</>
|
||||||
{archived && (
|
)}
|
||||||
<div className={s.actionItem} onClick={() => onOperate('un_archive')}>
|
{!archived && <div className={s.actionItem} onClick={() => onOperate('archive')}>
|
||||||
<ArchiveIcon />
|
<ArchiveIcon />
|
||||||
<span className={s.actionName}>{t('datasetDocuments.list.action.unarchive')}</span>
|
<span className={s.actionName}>{t('datasetDocuments.list.action.archive')}</span>
|
||||||
|
</div>}
|
||||||
|
{archived && (
|
||||||
|
<div className={s.actionItem} onClick={() => onOperate('un_archive')}>
|
||||||
|
<ArchiveIcon />
|
||||||
|
<span className={s.actionName}>{t('datasetDocuments.list.action.unarchive')}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={cn(s.actionItem, s.deleteActionItem, 'group')} onClick={() => setShowModal(true)}>
|
||||||
|
<TrashIcon className={'w-4 h-4 stroke-current text-gray-500 stroke-2 group-hover:text-red-500'} />
|
||||||
|
<span className={cn(s.actionName, 'group-hover:text-red-500')}>{t('datasetDocuments.list.action.delete')}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div className={cn(s.actionItem, s.deleteActionItem, 'group')} onClick={() => setShowModal(true)}>
|
|
||||||
<TrashIcon className={'w-4 h-4 stroke-current text-gray-500 stroke-2 group-hover:text-red-500'} />
|
|
||||||
<span className={cn(s.actionName, 'group-hover:text-red-500')}>{t('datasetDocuments.list.action.delete')}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
trigger='click'
|
||||||
trigger='click'
|
position='br'
|
||||||
position='br'
|
btnElement={
|
||||||
btnElement={
|
<div className={cn(s.commonIcon)}>
|
||||||
<div className={cn(s.commonIcon)}>
|
<DotsHorizontal className='w-4 h-4 text-gray-700' />
|
||||||
<DotsHorizontal className='w-4 h-4 text-gray-700' />
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
btnClassName={open => cn(isListScene ? s.actionIconWrapperList : s.actionIconWrapperDetail, open ? '!bg-gray-100 !shadow-none' : '!bg-transparent')}
|
||||||
btnClassName={open => cn(isListScene ? s.actionIconWrapperList : s.actionIconWrapperDetail, open ? '!bg-gray-100 !shadow-none' : '!bg-transparent')}
|
className={`!w-[200px] h-fit !z-20 ${className}`}
|
||||||
className={`!w-[200px] h-fit !z-20 ${className}`}
|
/>
|
||||||
/>
|
)}
|
||||||
{showModal && <Modal isShow={showModal} onClose={() => setShowModal(false)} className={s.delModal} closable>
|
{showModal && <Modal isShow={showModal} onClose={() => setShowModal(false)} className={s.delModal} closable>
|
||||||
<div>
|
<div>
|
||||||
<div className={s.warningWrapper}>
|
<div className={s.warningWrapper}>
|
||||||
@ -277,6 +285,7 @@ const renderCount = (count: number | undefined) => {
|
|||||||
|
|
||||||
type LocalDoc = SimpleDocumentDetail & { percent?: number }
|
type LocalDoc = SimpleDocumentDetail & { percent?: number }
|
||||||
type IDocumentListProps = {
|
type IDocumentListProps = {
|
||||||
|
embeddingAvailable: boolean
|
||||||
documents: LocalDoc[]
|
documents: LocalDoc[]
|
||||||
datasetId: string
|
datasetId: string
|
||||||
onUpdate: () => void
|
onUpdate: () => void
|
||||||
@ -285,7 +294,7 @@ type IDocumentListProps = {
|
|||||||
/**
|
/**
|
||||||
* Document list component including basic information
|
* Document list component including basic information
|
||||||
*/
|
*/
|
||||||
const DocumentList: FC<IDocumentListProps> = ({ documents = [], datasetId, onUpdate }) => {
|
const DocumentList: FC<IDocumentListProps> = ({ embeddingAvailable, documents = [], datasetId, onUpdate }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [localDocs, setLocalDocs] = useState<LocalDoc[]>(documents)
|
const [localDocs, setLocalDocs] = useState<LocalDoc[]>(documents)
|
||||||
@ -361,6 +370,7 @@ const DocumentList: FC<IDocumentListProps> = ({ documents = [], datasetId, onUpd
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<OperationAction
|
<OperationAction
|
||||||
|
embeddingAvailable={embeddingAvailable}
|
||||||
datasetId={datasetId}
|
datasetId={datasetId}
|
||||||
detail={pick(doc, ['enabled', 'archived', 'id', 'data_source_type', 'doc_form'])}
|
detail={pick(doc, ['enabled', 'archived', 'id', 'data_source_type', 'doc_form'])}
|
||||||
onUpdate={onUpdate}
|
onUpdate={onUpdate}
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import React, { useState, FC, useMemo } from 'react'
|
import type { FC } from 'react'
|
||||||
|
import React, { useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import { fetchTestingRecords } from '@/service/datasets'
|
|
||||||
import { omit } from 'lodash-es'
|
import { omit } from 'lodash-es'
|
||||||
import Pagination from '@/app/components/base/pagination'
|
|
||||||
import Modal from '@/app/components/base/modal'
|
|
||||||
import Loading from '@/app/components/base/loading'
|
|
||||||
import type { HitTestingResponse, HitTesting } from '@/models/datasets'
|
|
||||||
import cn from 'classnames'
|
import cn from 'classnames'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import SegmentCard from '../documents/detail/completed/SegmentCard'
|
import SegmentCard from '../documents/detail/completed/SegmentCard'
|
||||||
@ -15,8 +11,13 @@ import docStyle from '../documents/detail/completed/style.module.css'
|
|||||||
import Textarea from './textarea'
|
import Textarea from './textarea'
|
||||||
import s from './style.module.css'
|
import s from './style.module.css'
|
||||||
import HitDetail from './hit-detail'
|
import HitDetail from './hit-detail'
|
||||||
|
import type { HitTestingResponse, HitTesting as HitTestingType } from '@/models/datasets'
|
||||||
|
import Loading from '@/app/components/base/loading'
|
||||||
|
import Modal from '@/app/components/base/modal'
|
||||||
|
import Pagination from '@/app/components/base/pagination'
|
||||||
|
import { fetchTestingRecords } from '@/service/datasets'
|
||||||
|
|
||||||
const limit = 10;
|
const limit = 10
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
datasetId: string
|
datasetId: string
|
||||||
@ -34,23 +35,23 @@ const RecordsEmpty: FC = () => {
|
|||||||
|
|
||||||
const HitTesting: FC<Props> = ({ datasetId }: Props) => {
|
const HitTesting: FC<Props> = ({ datasetId }: Props) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [hitResult, setHitResult] = useState<HitTestingResponse | undefined>(); // 初始化记录为空数组
|
const [hitResult, setHitResult] = useState<HitTestingResponse | undefined>() // 初始化记录为空数组
|
||||||
const [submitLoading, setSubmitLoading] = useState(false);
|
const [submitLoading, setSubmitLoading] = useState(false)
|
||||||
const [currParagraph, setCurrParagraph] = useState<{ paraInfo?: HitTesting; showModal: boolean }>({ showModal: false })
|
const [currParagraph, setCurrParagraph] = useState<{ paraInfo?: HitTestingType; showModal: boolean }>({ showModal: false })
|
||||||
const [text, setText] = useState('');
|
const [text, setText] = useState('')
|
||||||
|
|
||||||
const [currPage, setCurrPage] = React.useState<number>(0)
|
const [currPage, setCurrPage] = React.useState<number>(0)
|
||||||
const { data: recordsRes, error, mutate: recordsMutate } = useSWR({
|
const { data: recordsRes, error, mutate: recordsMutate } = useSWR({
|
||||||
action: 'fetchTestingRecords',
|
action: 'fetchTestingRecords',
|
||||||
datasetId,
|
datasetId,
|
||||||
params: { limit, page: currPage + 1, }
|
params: { limit, page: currPage + 1 },
|
||||||
}, apiParams => fetchTestingRecords(omit(apiParams, 'action')))
|
}, apiParams => fetchTestingRecords(omit(apiParams, 'action')))
|
||||||
|
|
||||||
const total = recordsRes?.total || 0
|
const total = recordsRes?.total || 0
|
||||||
|
|
||||||
const points = useMemo(() => (hitResult?.records.map((v) => [v.tsne_position.x, v.tsne_position.y]) || []), [hitResult?.records])
|
const points = useMemo(() => (hitResult?.records.map(v => [v.tsne_position.x, v.tsne_position.y]) || []), [hitResult?.records])
|
||||||
|
|
||||||
const onClickCard = (detail: HitTesting) => {
|
const onClickCard = (detail: HitTestingType) => {
|
||||||
setCurrParagraph({ paraInfo: detail, showModal: true })
|
setCurrParagraph({ paraInfo: detail, showModal: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,50 +72,56 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
|
|||||||
text={text}
|
text={text}
|
||||||
/>
|
/>
|
||||||
<div className={cn(s.title, 'mt-8 mb-2')}>{t('datasetHitTesting.recents')}</div>
|
<div className={cn(s.title, 'mt-8 mb-2')}>{t('datasetHitTesting.recents')}</div>
|
||||||
{!recordsRes && !error ? (
|
{(!recordsRes && !error)
|
||||||
<div className='flex-1'><Loading type='app' /></div>
|
? (
|
||||||
) : recordsRes?.data?.length ? (
|
<div className='flex-1'><Loading type='app' /></div>
|
||||||
<>
|
)
|
||||||
<table className={`w-full border-collapse border-0 mt-3 ${s.table}`}>
|
: recordsRes?.data?.length
|
||||||
<thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-bold">
|
? (
|
||||||
<tr>
|
<>
|
||||||
<td className='w-28'>{t('datasetHitTesting.table.header.source')}</td>
|
<div className='grow overflow-y-auto'>
|
||||||
<td>{t('datasetHitTesting.table.header.text')}</td>
|
<table className={`w-full border-collapse border-0 mt-3 ${s.table}`}>
|
||||||
<td className='w-48'>{t('datasetHitTesting.table.header.time')}</td>
|
<thead className="sticky top-0 h-8 bg-white leading-8 border-b border-gray-200 text-gray-500 font-bold">
|
||||||
</tr>
|
<tr>
|
||||||
</thead>
|
<td className='w-28'>{t('datasetHitTesting.table.header.source')}</td>
|
||||||
<tbody className="text-gray-500">
|
<td>{t('datasetHitTesting.table.header.text')}</td>
|
||||||
{recordsRes?.data?.map((record) => {
|
<td className='w-48'>{t('datasetHitTesting.table.header.time')}</td>
|
||||||
return <tr
|
</tr>
|
||||||
key={record.id}
|
</thead>
|
||||||
className='group border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer'
|
<tbody className="text-gray-500">
|
||||||
onClick={() => setText(record.content)}
|
{recordsRes?.data?.map((record) => {
|
||||||
>
|
return <tr
|
||||||
<td className='w-24'>
|
key={record.id}
|
||||||
<div className='flex items-center'>
|
className='group border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer'
|
||||||
<div className={cn(s[`${record.source}_icon`], s.commonIcon, 'mr-1')} />
|
onClick={() => setText(record.content)}
|
||||||
<span className='capitalize'>{record.source.replace('_', ' ')}</span>
|
>
|
||||||
</div>
|
<td className='w-24'>
|
||||||
</td>
|
<div className='flex items-center'>
|
||||||
<td className='max-w-xs group-hover:text-primary-600'>{record.content}</td>
|
<div className={cn(s[`${record.source}_icon`], s.commonIcon, 'mr-1')} />
|
||||||
<td className='w-36'>
|
<span className='capitalize'>{record.source.replace('_', ' ')}</span>
|
||||||
{dayjs.unix(record.created_at).format(t('datasetHitTesting.dateTimeFormat') as string)}
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
<td className='max-w-xs group-hover:text-primary-600'>{record.content}</td>
|
||||||
})}
|
<td className='w-36'>
|
||||||
</tbody>
|
{dayjs.unix(record.created_at).format(t('datasetHitTesting.dateTimeFormat') as string)}
|
||||||
</table>
|
</td>
|
||||||
{(total && total > limit)
|
</tr>
|
||||||
? <Pagination current={currPage} onChange={setCurrPage} total={total} limit={limit} />
|
})}
|
||||||
: null}
|
</tbody>
|
||||||
</>
|
</table>
|
||||||
) : (
|
</div>
|
||||||
<RecordsEmpty />
|
{(total && total > limit)
|
||||||
)}
|
? <Pagination current={currPage} onChange={setCurrPage} total={total} limit={limit} />
|
||||||
|
: null}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<RecordsEmpty />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={s.rightDiv}>
|
<div className={s.rightDiv}>
|
||||||
{submitLoading ?
|
{submitLoading
|
||||||
<div className={s.cardWrapper}>
|
? <div className={s.cardWrapper}>
|
||||||
<SegmentCard
|
<SegmentCard
|
||||||
loading={true}
|
loading={true}
|
||||||
scene='hitTesting'
|
scene='hitTesting'
|
||||||
@ -125,33 +132,36 @@ const HitTesting: FC<Props> = ({ datasetId }: Props) => {
|
|||||||
scene='hitTesting'
|
scene='hitTesting'
|
||||||
className='h-[216px]'
|
className='h-[216px]'
|
||||||
/>
|
/>
|
||||||
</div> : !hitResult?.records.length ? (
|
</div>
|
||||||
<div className='h-full flex flex-col justify-center items-center'>
|
: !hitResult?.records.length
|
||||||
<div className={cn(docStyle.commonIcon, docStyle.targetIcon, '!bg-gray-200 !h-14 !w-14')} />
|
? (
|
||||||
<div className='text-gray-300 text-[13px] mt-3'>
|
<div className='h-full flex flex-col justify-center items-center'>
|
||||||
{t('datasetHitTesting.hit.emptyTip')}
|
<div className={cn(docStyle.commonIcon, docStyle.targetIcon, '!bg-gray-200 !h-14 !w-14')} />
|
||||||
</div>
|
<div className='text-gray-300 text-[13px] mt-3'>
|
||||||
</div>
|
{t('datasetHitTesting.hit.emptyTip')}
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className='text-gray-600 font-semibold mb-4'>{t('datasetHitTesting.hit.title')}</div>
|
|
||||||
<div className='overflow-auto flex-1'>
|
|
||||||
<div className={s.cardWrapper}>
|
|
||||||
{hitResult?.records.map((record, idx) => {
|
|
||||||
return <SegmentCard
|
|
||||||
key={idx}
|
|
||||||
loading={false}
|
|
||||||
detail={record.segment as any}
|
|
||||||
score={record.score}
|
|
||||||
scene='hitTesting'
|
|
||||||
className='h-[216px] mb-4'
|
|
||||||
onClick={() => onClickCard(record as any)}
|
|
||||||
/>
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
)
|
||||||
)
|
: (
|
||||||
|
<>
|
||||||
|
<div className='text-gray-600 font-semibold mb-4'>{t('datasetHitTesting.hit.title')}</div>
|
||||||
|
<div className='overflow-auto flex-1'>
|
||||||
|
<div className={s.cardWrapper}>
|
||||||
|
{hitResult?.records.map((record, idx) => {
|
||||||
|
return <SegmentCard
|
||||||
|
key={idx}
|
||||||
|
loading={false}
|
||||||
|
detail={record.segment as any}
|
||||||
|
score={record.score}
|
||||||
|
scene='hitTesting'
|
||||||
|
className='h-[216px] mb-4'
|
||||||
|
onClick={() => onClickCard(record as any)}
|
||||||
|
/>
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -5,6 +5,7 @@ import useSWR from 'swr'
|
|||||||
import { useContext } from 'use-context-selector'
|
import { useContext } from 'use-context-selector'
|
||||||
import { BookOpenIcon } from '@heroicons/react/24/outline'
|
import { BookOpenIcon } from '@heroicons/react/24/outline'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import cn from 'classnames'
|
||||||
import PermissionsRadio from '../permissions-radio'
|
import PermissionsRadio from '../permissions-radio'
|
||||||
import IndexMethodRadio from '../index-method-radio'
|
import IndexMethodRadio from '../index-method-radio'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
@ -88,7 +89,8 @@ const Form = ({
|
|||||||
<div>{t('datasetSettings.form.name')}</div>
|
<div>{t('datasetSettings.form.name')}</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
className={inputClass}
|
disabled={!currentDataset?.embedding_available}
|
||||||
|
className={cn(inputClass, !currentDataset?.embedding_available && 'opacity-60')}
|
||||||
value={name}
|
value={name}
|
||||||
onChange={e => setName(e.target.value)}
|
onChange={e => setName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -99,7 +101,8 @@ const Form = ({
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<textarea
|
<textarea
|
||||||
className={`${inputClass} block mb-2 h-[120px] py-2 resize-none`}
|
disabled={!currentDataset?.embedding_available}
|
||||||
|
className={cn(`${inputClass} block mb-2 h-[120px] py-2 resize-none`, !currentDataset?.embedding_available && 'opacity-60')}
|
||||||
placeholder={t('datasetSettings.form.descPlaceholder') || ''}
|
placeholder={t('datasetSettings.form.descPlaceholder') || ''}
|
||||||
value={description}
|
value={description}
|
||||||
onChange={e => setDescription(e.target.value)}
|
onChange={e => setDescription(e.target.value)}
|
||||||
@ -116,61 +119,67 @@ const Form = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className='w-[480px]'>
|
<div className='w-[480px]'>
|
||||||
<PermissionsRadio
|
<PermissionsRadio
|
||||||
|
disable={!currentDataset?.embedding_available}
|
||||||
value={permission}
|
value={permission}
|
||||||
onChange={v => setPermission(v)}
|
onChange={v => setPermission(v)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full h-0 border-b-[0.5px] border-b-gray-200 my-2' />
|
{currentDataset && currentDataset.indexing_technique && (
|
||||||
<div className={rowClass}>
|
<>
|
||||||
<div className={labelClass}>
|
<div className='w-full h-0 border-b-[0.5px] border-b-gray-200 my-2' />
|
||||||
<div>{t('datasetSettings.form.indexMethod')}</div>
|
<div className={rowClass}>
|
||||||
|
<div className={labelClass}>
|
||||||
|
<div>{t('datasetSettings.form.indexMethod')}</div>
|
||||||
|
</div>
|
||||||
|
<div className='w-[480px]'>
|
||||||
|
<IndexMethodRadio
|
||||||
|
disable={!currentDataset?.embedding_available}
|
||||||
|
value={indexMethod}
|
||||||
|
onChange={v => setIndexMethod(v)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{currentDataset && currentDataset.indexing_technique === 'high_quality' && (
|
||||||
|
<div className={rowClass}>
|
||||||
|
<div className={labelClass}>
|
||||||
|
<div>{t('datasetSettings.form.embeddingModel')}</div>
|
||||||
|
</div>
|
||||||
|
<div className='w-[480px]'>
|
||||||
|
<div className='w-full h-9 rounded-lg bg-gray-100 opacity-60'>
|
||||||
|
<ModelSelector
|
||||||
|
readonly
|
||||||
|
value={{
|
||||||
|
providerName: currentDataset.embedding_model_provider as ProviderEnum,
|
||||||
|
modelName: currentDataset.embedding_model,
|
||||||
|
}}
|
||||||
|
modelType={ModelType.embeddings}
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='mt-2 w-full text-xs leading-6 text-gray-500'>
|
||||||
|
{t('datasetSettings.form.embeddingModelTip')}
|
||||||
|
<span className='text-[#155eef] cursor-pointer' onClick={() => setShowSetAPIKeyModal(true)}>{t('datasetSettings.form.embeddingModelTipLink')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='w-[480px]'>
|
)}
|
||||||
<IndexMethodRadio
|
{currentDataset?.embedding_available && (
|
||||||
value={indexMethod}
|
<div className={rowClass}>
|
||||||
onChange={v => setIndexMethod(v)}
|
<div className={labelClass} />
|
||||||
/>
|
<div className='w-[480px]'>
|
||||||
|
<Button
|
||||||
|
className='min-w-24 text-sm'
|
||||||
|
type='primary'
|
||||||
|
onClick={handleSave}
|
||||||
|
>
|
||||||
|
{t('datasetSettings.form.save')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div className={rowClass}>
|
|
||||||
<div className={labelClass}>
|
|
||||||
<div>{t('datasetSettings.form.embeddingModel')}</div>
|
|
||||||
</div>
|
|
||||||
<div className='w-[480px]'>
|
|
||||||
{currentDataset && (
|
|
||||||
<>
|
|
||||||
<div className='w-full h-9 rounded-lg bg-gray-100 opacity-60'>
|
|
||||||
<ModelSelector
|
|
||||||
readonly
|
|
||||||
value={{
|
|
||||||
providerName: currentDataset.embedding_model_provider as ProviderEnum,
|
|
||||||
modelName: currentDataset.embedding_model,
|
|
||||||
}}
|
|
||||||
modelType={ModelType.embeddings}
|
|
||||||
onChange={() => {}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='mt-2 w-full text-xs leading-6 text-gray-500'>
|
|
||||||
{t('datasetSettings.form.embeddingModelTip')}
|
|
||||||
<span className='text-[#155eef] cursor-pointer' onClick={() => setShowSetAPIKeyModal(true)}>{t('datasetSettings.form.embeddingModelTipLink')}</span>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={rowClass}>
|
|
||||||
<div className={labelClass} />
|
|
||||||
<div className='w-[480px]'>
|
|
||||||
<Button
|
|
||||||
className='min-w-24 text-sm'
|
|
||||||
type='primary'
|
|
||||||
onClick={handleSave}
|
|
||||||
>
|
|
||||||
{t('datasetSettings.form.save')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{showSetAPIKeyModal && (
|
{showSetAPIKeyModal && (
|
||||||
<AccountSetting activeTab="provider" onCancel={async () => {
|
<AccountSetting activeTab="provider" onCancel={async () => {
|
||||||
setShowSetAPIKeyModal(false)
|
setShowSetAPIKeyModal(false)
|
||||||
|
@ -36,3 +36,19 @@
|
|||||||
border-color: #528BFF;
|
border-color: #528BFF;
|
||||||
box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), 0px 1px 2px rgba(16, 24, 40, 0.06);
|
box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), 0px 1px 2px rgba(16, 24, 40, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapper .item.disable {
|
||||||
|
@apply opacity-60;
|
||||||
|
}
|
||||||
|
.wrapper .item-active.disable {
|
||||||
|
@apply opacity-60;
|
||||||
|
}
|
||||||
|
.wrapper .item.disable:hover {
|
||||||
|
@apply bg-gray-25 border border-gray-100 shadow-none cursor-default opacity-60;
|
||||||
|
}
|
||||||
|
.wrapper .item-active.disable:hover {
|
||||||
|
@apply cursor-default opacity-60;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import s from './index.module.css'
|
import s from './index.module.css'
|
||||||
import { DataSet } from '@/models/datasets'
|
import type { DataSet } from '@/models/datasets'
|
||||||
|
|
||||||
const itemClass = `
|
const itemClass = `
|
||||||
w-[234px] p-3 rounded-xl bg-gray-25 border border-gray-100 cursor-pointer
|
w-[234px] p-3 rounded-xl bg-gray-25 border border-gray-100 cursor-pointer
|
||||||
@ -13,11 +13,13 @@ const radioClass = `
|
|||||||
type IIndexMethodRadioProps = {
|
type IIndexMethodRadioProps = {
|
||||||
value?: DataSet['indexing_technique']
|
value?: DataSet['indexing_technique']
|
||||||
onChange: (v?: DataSet['indexing_technique']) => void
|
onChange: (v?: DataSet['indexing_technique']) => void
|
||||||
|
disable?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const IndexMethodRadio = ({
|
const IndexMethodRadio = ({
|
||||||
value,
|
value,
|
||||||
onChange
|
onChange,
|
||||||
|
disable,
|
||||||
}: IIndexMethodRadioProps) => {
|
}: IIndexMethodRadioProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const options = [
|
const options = [
|
||||||
@ -25,14 +27,14 @@ const IndexMethodRadio = ({
|
|||||||
key: 'high_quality',
|
key: 'high_quality',
|
||||||
text: t('datasetSettings.form.indexMethodHighQuality'),
|
text: t('datasetSettings.form.indexMethodHighQuality'),
|
||||||
desc: t('datasetSettings.form.indexMethodHighQualityTip'),
|
desc: t('datasetSettings.form.indexMethodHighQualityTip'),
|
||||||
icon: 'high-quality'
|
icon: 'high-quality',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'economy',
|
key: 'economy',
|
||||||
text: t('datasetSettings.form.indexMethodEconomy'),
|
text: t('datasetSettings.form.indexMethodEconomy'),
|
||||||
desc: t('datasetSettings.form.indexMethodEconomyTip'),
|
desc: t('datasetSettings.form.indexMethodEconomyTip'),
|
||||||
icon: 'economy'
|
icon: 'economy',
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -42,11 +44,15 @@ const IndexMethodRadio = ({
|
|||||||
<div
|
<div
|
||||||
key={option.key}
|
key={option.key}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
option.key === value && s['item-active'],
|
itemClass,
|
||||||
s.item,
|
s.item,
|
||||||
itemClass
|
option.key === value && s['item-active'],
|
||||||
|
disable && s.disable,
|
||||||
)}
|
)}
|
||||||
onClick={() => onChange(option.key as DataSet['indexing_technique'])}
|
onClick={() => {
|
||||||
|
if (!disable)
|
||||||
|
onChange(option.key as DataSet['indexing_technique'])
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className='flex items-center mb-1'>
|
<div className='flex items-center mb-1'>
|
||||||
<div className={classNames(s.icon, s[`${option.icon}-icon`])} />
|
<div className={classNames(s.icon, s[`${option.icon}-icon`])} />
|
||||||
|
@ -28,3 +28,19 @@
|
|||||||
border-color: #528BFF;
|
border-color: #528BFF;
|
||||||
box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), 0px 1px 2px rgba(16, 24, 40, 0.06);
|
box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), 0px 1px 2px rgba(16, 24, 40, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapper .item.disable {
|
||||||
|
@apply opacity-60;
|
||||||
|
}
|
||||||
|
.wrapper .item-active.disable {
|
||||||
|
@apply opacity-60;
|
||||||
|
}
|
||||||
|
.wrapper .item.disable:hover {
|
||||||
|
@apply bg-gray-25 border border-gray-100 shadow-none cursor-default opacity-60;
|
||||||
|
}
|
||||||
|
.wrapper .item-active.disable:hover {
|
||||||
|
@apply cursor-default opacity-60;
|
||||||
|
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);
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import s from './index.module.css'
|
import s from './index.module.css'
|
||||||
import { DataSet } from '@/models/datasets'
|
import type { DataSet } from '@/models/datasets'
|
||||||
|
|
||||||
const itemClass = `
|
const itemClass = `
|
||||||
flex items-center w-[234px] h-12 px-3 rounded-xl bg-gray-25 border border-gray-100 cursor-pointer
|
flex items-center w-[234px] h-12 px-3 rounded-xl bg-gray-25 border border-gray-100 cursor-pointer
|
||||||
@ -13,22 +13,24 @@ const radioClass = `
|
|||||||
type IPermissionsRadioProps = {
|
type IPermissionsRadioProps = {
|
||||||
value?: DataSet['permission']
|
value?: DataSet['permission']
|
||||||
onChange: (v?: DataSet['permission']) => void
|
onChange: (v?: DataSet['permission']) => void
|
||||||
|
disable?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const PermissionsRadio = ({
|
const PermissionsRadio = ({
|
||||||
value,
|
value,
|
||||||
onChange
|
onChange,
|
||||||
|
disable,
|
||||||
}: IPermissionsRadioProps) => {
|
}: IPermissionsRadioProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const options = [
|
const options = [
|
||||||
{
|
{
|
||||||
key: 'only_me',
|
key: 'only_me',
|
||||||
text: t('datasetSettings.form.permissionsOnlyMe')
|
text: t('datasetSettings.form.permissionsOnlyMe'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'all_team_members',
|
key: 'all_team_members',
|
||||||
text: t('datasetSettings.form.permissionsAllMember')
|
text: t('datasetSettings.form.permissionsAllMember'),
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -38,11 +40,15 @@ const PermissionsRadio = ({
|
|||||||
<div
|
<div
|
||||||
key={option.key}
|
key={option.key}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
option.key === value && s['item-active'],
|
|
||||||
itemClass,
|
itemClass,
|
||||||
s.item
|
s.item,
|
||||||
|
option.key === value && s['item-active'],
|
||||||
|
disable && s.disable,
|
||||||
)}
|
)}
|
||||||
onClick={() => onChange(option.key as DataSet['permission'])}
|
onClick={() => {
|
||||||
|
if (!disable)
|
||||||
|
onChange(option.key as DataSet['permission'])
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className={classNames(s['user-icon'], 'mr-3')} />
|
<div className={classNames(s['user-icon'], 'mr-3')} />
|
||||||
<div className='grow text-sm text-gray-900'>{option.text}</div>
|
<div className='grow text-sm text-gray-900'>{option.text}</div>
|
||||||
|
@ -35,6 +35,8 @@ import exploreEn from './lang/explore.en'
|
|||||||
import exploreZh from './lang/explore.zh'
|
import exploreZh from './lang/explore.zh'
|
||||||
import { getLocaleOnClient } from '@/i18n/client'
|
import { getLocaleOnClient } from '@/i18n/client'
|
||||||
|
|
||||||
|
const localLng = getLocaleOnClient()
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
'en': {
|
'en': {
|
||||||
translation: {
|
translation: {
|
||||||
@ -86,7 +88,7 @@ i18n.use(initReactI18next)
|
|||||||
// init i18next
|
// init i18next
|
||||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||||
.init({
|
.init({
|
||||||
lng: getLocaleOnClient(),
|
lng: localLng,
|
||||||
fallbackLng: 'en',
|
fallbackLng: 'en',
|
||||||
// debug: true,
|
// debug: true,
|
||||||
resources,
|
resources,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user