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