Feat annotations panel (#22968)
This commit is contained in:
parent
c05c5953a8
commit
4499cda186
8 changed files with 262 additions and 8 deletions
|
|
@ -131,6 +131,22 @@ class AnnotationListApi(Resource):
|
||||||
raise Forbidden()
|
raise Forbidden()
|
||||||
|
|
||||||
app_id = str(app_id)
|
app_id = str(app_id)
|
||||||
|
|
||||||
|
# Use request.args.getlist to get annotation_ids array directly
|
||||||
|
annotation_ids = request.args.getlist("annotation_id")
|
||||||
|
|
||||||
|
# If annotation_ids are provided, handle batch deletion
|
||||||
|
if annotation_ids:
|
||||||
|
if not annotation_ids:
|
||||||
|
return {
|
||||||
|
"code": "bad_request",
|
||||||
|
"message": "annotation_ids are required if the parameter is provided.",
|
||||||
|
}, 400
|
||||||
|
|
||||||
|
result = AppAnnotationService.delete_app_annotations_in_batch(app_id, annotation_ids)
|
||||||
|
return result, 204
|
||||||
|
# If no annotation_ids are provided, handle clearing all annotations
|
||||||
|
else:
|
||||||
AppAnnotationService.clear_all_annotations(app_id)
|
AppAnnotationService.clear_all_annotations(app_id)
|
||||||
return {"result": "success"}, 204
|
return {"result": "success"}, 204
|
||||||
|
|
||||||
|
|
@ -278,6 +294,7 @@ api.add_resource(
|
||||||
)
|
)
|
||||||
api.add_resource(AnnotationListApi, "/apps/<uuid:app_id>/annotations")
|
api.add_resource(AnnotationListApi, "/apps/<uuid:app_id>/annotations")
|
||||||
api.add_resource(AnnotationExportApi, "/apps/<uuid:app_id>/annotations/export")
|
api.add_resource(AnnotationExportApi, "/apps/<uuid:app_id>/annotations/export")
|
||||||
|
api.add_resource(AnnotationCreateApi, "/apps/<uuid:app_id>/annotations")
|
||||||
api.add_resource(AnnotationUpdateDeleteApi, "/apps/<uuid:app_id>/annotations/<uuid:annotation_id>")
|
api.add_resource(AnnotationUpdateDeleteApi, "/apps/<uuid:app_id>/annotations/<uuid:annotation_id>")
|
||||||
api.add_resource(AnnotationBatchImportApi, "/apps/<uuid:app_id>/annotations/batch-import")
|
api.add_resource(AnnotationBatchImportApi, "/apps/<uuid:app_id>/annotations/batch-import")
|
||||||
api.add_resource(AnnotationBatchImportStatusApi, "/apps/<uuid:app_id>/annotations/batch-import-status/<uuid:job_id>")
|
api.add_resource(AnnotationBatchImportStatusApi, "/apps/<uuid:app_id>/annotations/batch-import-status/<uuid:job_id>")
|
||||||
|
|
|
||||||
|
|
@ -266,6 +266,54 @@ class AppAnnotationService:
|
||||||
annotation.id, app_id, current_user.current_tenant_id, app_annotation_setting.collection_binding_id
|
annotation.id, app_id, current_user.current_tenant_id, app_annotation_setting.collection_binding_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete_app_annotations_in_batch(cls, app_id: str, annotation_ids: list[str]):
|
||||||
|
# get app info
|
||||||
|
app = (
|
||||||
|
db.session.query(App)
|
||||||
|
.where(App.id == app_id, App.tenant_id == current_user.current_tenant_id, App.status == "normal")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not app:
|
||||||
|
raise NotFound("App not found")
|
||||||
|
|
||||||
|
# Fetch annotations and their settings in a single query
|
||||||
|
annotations_to_delete = (
|
||||||
|
db.session.query(MessageAnnotation, AppAnnotationSetting)
|
||||||
|
.outerjoin(AppAnnotationSetting, MessageAnnotation.app_id == AppAnnotationSetting.app_id)
|
||||||
|
.filter(MessageAnnotation.id.in_(annotation_ids))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not annotations_to_delete:
|
||||||
|
return {"deleted_count": 0}
|
||||||
|
|
||||||
|
# Step 1: Extract IDs for bulk operations
|
||||||
|
annotation_ids_to_delete = [annotation.id for annotation, _ in annotations_to_delete]
|
||||||
|
|
||||||
|
# Step 2: Bulk delete hit histories in a single query
|
||||||
|
db.session.query(AppAnnotationHitHistory).filter(
|
||||||
|
AppAnnotationHitHistory.annotation_id.in_(annotation_ids_to_delete)
|
||||||
|
).delete(synchronize_session=False)
|
||||||
|
|
||||||
|
# Step 3: Trigger async tasks for search index deletion
|
||||||
|
for annotation, annotation_setting in annotations_to_delete:
|
||||||
|
if annotation_setting:
|
||||||
|
delete_annotation_index_task.delay(
|
||||||
|
annotation.id, app_id, current_user.current_tenant_id, annotation_setting.collection_binding_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 4: Bulk delete annotations in a single query
|
||||||
|
deleted_count = (
|
||||||
|
db.session.query(MessageAnnotation)
|
||||||
|
.filter(MessageAnnotation.id.in_(annotation_ids_to_delete))
|
||||||
|
.delete(synchronize_session=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return {"deleted_count": deleted_count}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def batch_import_app_annotations(cls, app_id, file: FileStorage) -> dict:
|
def batch_import_app_annotations(cls, app_id, file: FileStorage) -> dict:
|
||||||
# get app info
|
# get app info
|
||||||
|
|
|
||||||
79
web/app/components/app/annotation/batch-action.tsx
Normal file
79
web/app/components/app/annotation/batch-action.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import React, { type FC } from 'react'
|
||||||
|
import { RiDeleteBinLine } from '@remixicon/react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useBoolean } from 'ahooks'
|
||||||
|
import Divider from '@/app/components/base/divider'
|
||||||
|
import classNames from '@/utils/classnames'
|
||||||
|
import Confirm from '@/app/components/base/confirm'
|
||||||
|
|
||||||
|
const i18nPrefix = 'appAnnotation.batchAction'
|
||||||
|
|
||||||
|
type IBatchActionProps = {
|
||||||
|
className?: string
|
||||||
|
selectedIds: string[]
|
||||||
|
onBatchDelete: () => Promise<void>
|
||||||
|
onCancel: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const BatchAction: FC<IBatchActionProps> = ({
|
||||||
|
className,
|
||||||
|
selectedIds,
|
||||||
|
onBatchDelete,
|
||||||
|
onCancel,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [isShowDeleteConfirm, {
|
||||||
|
setTrue: showDeleteConfirm,
|
||||||
|
setFalse: hideDeleteConfirm,
|
||||||
|
}] = useBoolean(false)
|
||||||
|
const [isDeleting, {
|
||||||
|
setTrue: setIsDeleting,
|
||||||
|
setFalse: setIsNotDeleting,
|
||||||
|
}] = useBoolean(false)
|
||||||
|
|
||||||
|
const handleBatchDelete = async () => {
|
||||||
|
setIsDeleting()
|
||||||
|
await onBatchDelete()
|
||||||
|
hideDeleteConfirm()
|
||||||
|
setIsNotDeleting()
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={classNames('pointer-events-none flex w-full justify-center', className)}>
|
||||||
|
<div className='pointer-events-auto flex items-center gap-x-1 rounded-[10px] border border-components-actionbar-border-accent bg-components-actionbar-bg-accent p-1 shadow-xl shadow-shadow-shadow-5 backdrop-blur-[5px]'>
|
||||||
|
<div className='inline-flex items-center gap-x-2 py-1 pl-2 pr-3'>
|
||||||
|
<span className='flex h-5 w-5 items-center justify-center rounded-md bg-text-accent px-1 py-0.5 text-xs font-medium text-text-primary-on-surface'>
|
||||||
|
{selectedIds.length}
|
||||||
|
</span>
|
||||||
|
<span className='text-[13px] font-semibold leading-[16px] text-text-accent'>{t(`${i18nPrefix}.selected`)}</span>
|
||||||
|
</div>
|
||||||
|
<Divider type='vertical' className='mx-0.5 h-3.5 bg-divider-regular' />
|
||||||
|
<div className='flex cursor-pointer items-center gap-x-0.5 px-3 py-2' onClick={showDeleteConfirm}>
|
||||||
|
<RiDeleteBinLine className='h-4 w-4 text-components-button-destructive-ghost-text' />
|
||||||
|
<button type='button' className='px-0.5 text-[13px] font-medium leading-[16px] text-components-button-destructive-ghost-text' >
|
||||||
|
{t('common.operation.delete')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Divider type='vertical' className='mx-0.5 h-3.5 bg-divider-regular' />
|
||||||
|
<button type='button' className='px-3.5 py-2 text-[13px] font-medium leading-[16px] text-components-button-ghost-text' onClick={onCancel}>
|
||||||
|
{t('common.operation.cancel')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
isShowDeleteConfirm && (
|
||||||
|
<Confirm
|
||||||
|
isShow
|
||||||
|
title={t('appAnnotation.list.delete.title')}
|
||||||
|
confirmText={t('common.operation.delete')}
|
||||||
|
onConfirm={handleBatchDelete}
|
||||||
|
onCancel={hideDeleteConfirm}
|
||||||
|
isLoading={isDeleting}
|
||||||
|
isDisabled={isDeleting}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(BatchAction)
|
||||||
|
|
@ -26,6 +26,7 @@ import { useProviderContext } from '@/context/provider-context'
|
||||||
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
|
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
|
||||||
import type { App } from '@/types/app'
|
import type { App } from '@/types/app'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
import { delAnnotations } from '@/service/annotation'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
appDetail: App
|
appDetail: App
|
||||||
|
|
@ -50,7 +51,9 @@ const Annotation: FC<Props> = (props) => {
|
||||||
const [controlUpdateList, setControlUpdateList] = useState(Date.now())
|
const [controlUpdateList, setControlUpdateList] = useState(Date.now())
|
||||||
const [currItem, setCurrItem] = useState<AnnotationItem | null>(null)
|
const [currItem, setCurrItem] = useState<AnnotationItem | null>(null)
|
||||||
const [isShowViewModal, setIsShowViewModal] = useState(false)
|
const [isShowViewModal, setIsShowViewModal] = useState(false)
|
||||||
|
const [selectedIds, setSelectedIds] = useState<string[]>([])
|
||||||
const debouncedQueryParams = useDebounce(queryParams, { wait: 500 })
|
const debouncedQueryParams = useDebounce(queryParams, { wait: 500 })
|
||||||
|
const [isBatchDeleting, setIsBatchDeleting] = useState(false)
|
||||||
|
|
||||||
const fetchAnnotationConfig = async () => {
|
const fetchAnnotationConfig = async () => {
|
||||||
const res = await doFetchAnnotationConfig(appDetail.id)
|
const res = await doFetchAnnotationConfig(appDetail.id)
|
||||||
|
|
@ -60,7 +63,6 @@ const Annotation: FC<Props> = (props) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isChatApp) fetchAnnotationConfig()
|
if (isChatApp) fetchAnnotationConfig()
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const ensureJobCompleted = async (jobId: string, status: AnnotationEnableStatus) => {
|
const ensureJobCompleted = async (jobId: string, status: AnnotationEnableStatus) => {
|
||||||
|
|
@ -89,7 +91,6 @@ const Annotation: FC<Props> = (props) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchList(currPage + 1)
|
fetchList(currPage + 1)
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [currPage, limit, debouncedQueryParams])
|
}, [currPage, limit, debouncedQueryParams])
|
||||||
|
|
||||||
const handleAdd = async (payload: AnnotationItemBasic) => {
|
const handleAdd = async (payload: AnnotationItemBasic) => {
|
||||||
|
|
@ -106,6 +107,25 @@ const Annotation: FC<Props> = (props) => {
|
||||||
setControlUpdateList(Date.now())
|
setControlUpdateList(Date.now())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleBatchDelete = async () => {
|
||||||
|
if (isBatchDeleting)
|
||||||
|
return
|
||||||
|
setIsBatchDeleting(true)
|
||||||
|
try {
|
||||||
|
await delAnnotations(appDetail.id, selectedIds)
|
||||||
|
Toast.notify({ message: t('common.api.actionSuccess'), type: 'success' })
|
||||||
|
fetchList()
|
||||||
|
setControlUpdateList(Date.now())
|
||||||
|
setSelectedIds([])
|
||||||
|
}
|
||||||
|
catch (e: any) {
|
||||||
|
Toast.notify({ type: 'error', message: e.message || t('common.api.actionFailed') })
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setIsBatchDeleting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleView = (item: AnnotationItem) => {
|
const handleView = (item: AnnotationItem) => {
|
||||||
setCurrItem(item)
|
setCurrItem(item)
|
||||||
setIsShowViewModal(true)
|
setIsShowViewModal(true)
|
||||||
|
|
@ -189,6 +209,11 @@ const Annotation: FC<Props> = (props) => {
|
||||||
list={list}
|
list={list}
|
||||||
onRemove={handleRemove}
|
onRemove={handleRemove}
|
||||||
onView={handleView}
|
onView={handleView}
|
||||||
|
selectedIds={selectedIds}
|
||||||
|
onSelectedIdsChange={setSelectedIds}
|
||||||
|
onBatchDelete={handleBatchDelete}
|
||||||
|
onCancel={() => setSelectedIds([])}
|
||||||
|
isBatchDeleting={isBatchDeleting}
|
||||||
/>
|
/>
|
||||||
: <div className='flex h-full grow items-center justify-center'><EmptyElement /></div>
|
: <div className='flex h-full grow items-center justify-center'><EmptyElement /></div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client'
|
'use client'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import React from 'react'
|
import React, { useCallback, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
|
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
|
||||||
import type { AnnotationItem } from './type'
|
import type { AnnotationItem } from './type'
|
||||||
|
|
@ -8,28 +8,67 @@ import RemoveAnnotationConfirmModal from './remove-annotation-confirm-modal'
|
||||||
import ActionButton from '@/app/components/base/action-button'
|
import ActionButton from '@/app/components/base/action-button'
|
||||||
import useTimestamp from '@/hooks/use-timestamp'
|
import useTimestamp from '@/hooks/use-timestamp'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
import Checkbox from '@/app/components/base/checkbox'
|
||||||
|
import BatchAction from './batch-action'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
list: AnnotationItem[]
|
list: AnnotationItem[]
|
||||||
onRemove: (id: string) => void
|
|
||||||
onView: (item: AnnotationItem) => void
|
onView: (item: AnnotationItem) => void
|
||||||
|
onRemove: (id: string) => void
|
||||||
|
selectedIds: string[]
|
||||||
|
onSelectedIdsChange: (selectedIds: string[]) => void
|
||||||
|
onBatchDelete: () => Promise<void>
|
||||||
|
onCancel: () => void
|
||||||
|
isBatchDeleting?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const List: FC<Props> = ({
|
const List: FC<Props> = ({
|
||||||
list,
|
list,
|
||||||
onView,
|
onView,
|
||||||
onRemove,
|
onRemove,
|
||||||
|
selectedIds,
|
||||||
|
onSelectedIdsChange,
|
||||||
|
onBatchDelete,
|
||||||
|
onCancel,
|
||||||
|
isBatchDeleting,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { formatTime } = useTimestamp()
|
const { formatTime } = useTimestamp()
|
||||||
const [currId, setCurrId] = React.useState<string | null>(null)
|
const [currId, setCurrId] = React.useState<string | null>(null)
|
||||||
const [showConfirmDelete, setShowConfirmDelete] = React.useState(false)
|
const [showConfirmDelete, setShowConfirmDelete] = React.useState(false)
|
||||||
|
|
||||||
|
const isAllSelected = useMemo(() => {
|
||||||
|
return list.length > 0 && list.every(item => selectedIds.includes(item.id))
|
||||||
|
}, [list, selectedIds])
|
||||||
|
|
||||||
|
const isSomeSelected = useMemo(() => {
|
||||||
|
return list.some(item => selectedIds.includes(item.id))
|
||||||
|
}, [list, selectedIds])
|
||||||
|
|
||||||
|
const handleSelectAll = useCallback(() => {
|
||||||
|
const currentPageIds = list.map(item => item.id)
|
||||||
|
const otherPageIds = selectedIds.filter(id => !currentPageIds.includes(id))
|
||||||
|
|
||||||
|
if (isAllSelected)
|
||||||
|
onSelectedIdsChange(otherPageIds)
|
||||||
|
else
|
||||||
|
onSelectedIdsChange([...otherPageIds, ...currentPageIds])
|
||||||
|
}, [isAllSelected, list, selectedIds, onSelectedIdsChange])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='overflow-x-auto'>
|
<div className='relative grow overflow-x-auto'>
|
||||||
<table className={cn('mt-2 w-full min-w-[440px] border-collapse border-0')}>
|
<table className={cn('mt-2 w-full min-w-[440px] border-collapse border-0')}>
|
||||||
<thead className='system-xs-medium-uppercase text-text-tertiary'>
|
<thead className='system-xs-medium-uppercase text-text-tertiary'>
|
||||||
<tr>
|
<tr>
|
||||||
<td className='w-5 whitespace-nowrap rounded-l-lg bg-background-section-burn pl-2 pr-1'>{t('appAnnotation.table.header.question')}</td>
|
<td className='w-12 whitespace-nowrap rounded-l-lg bg-background-section-burn px-2'>
|
||||||
|
<Checkbox
|
||||||
|
className='mr-2'
|
||||||
|
checked={isAllSelected}
|
||||||
|
indeterminate={!isAllSelected && isSomeSelected}
|
||||||
|
onCheck={handleSelectAll}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className='w-5 whitespace-nowrap bg-background-section-burn pl-2 pr-1'>{t('appAnnotation.table.header.question')}</td>
|
||||||
<td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>{t('appAnnotation.table.header.answer')}</td>
|
<td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>{t('appAnnotation.table.header.answer')}</td>
|
||||||
<td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>{t('appAnnotation.table.header.createdAt')}</td>
|
<td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>{t('appAnnotation.table.header.createdAt')}</td>
|
||||||
<td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>{t('appAnnotation.table.header.hits')}</td>
|
<td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>{t('appAnnotation.table.header.hits')}</td>
|
||||||
|
|
@ -47,6 +86,18 @@ const List: FC<Props> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<td className='w-12 px-2' onClick={e => e.stopPropagation()}>
|
||||||
|
<Checkbox
|
||||||
|
className='mr-2'
|
||||||
|
checked={selectedIds.includes(item.id)}
|
||||||
|
onCheck={() => {
|
||||||
|
if (selectedIds.includes(item.id))
|
||||||
|
onSelectedIdsChange(selectedIds.filter(id => id !== item.id))
|
||||||
|
else
|
||||||
|
onSelectedIdsChange([...selectedIds, item.id])
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td
|
<td
|
||||||
className='max-w-[250px] overflow-hidden text-ellipsis whitespace-nowrap p-3 pr-2'
|
className='max-w-[250px] overflow-hidden text-ellipsis whitespace-nowrap p-3 pr-2'
|
||||||
title={item.question}
|
title={item.question}
|
||||||
|
|
@ -85,6 +136,15 @@ const List: FC<Props> = ({
|
||||||
setShowConfirmDelete(false)
|
setShowConfirmDelete(false)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{selectedIds.length > 0 && (
|
||||||
|
<BatchAction
|
||||||
|
className='absolute bottom-6 left-1/2 z-20 -translate-x-1/2'
|
||||||
|
selectedIds={selectedIds}
|
||||||
|
onBatchDelete={onBatchDelete}
|
||||||
|
onCancel={onCancel}
|
||||||
|
isBatchDeleting={isBatchDeleting}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,16 @@ const translation = {
|
||||||
error: 'Import Error',
|
error: 'Import Error',
|
||||||
ok: 'OK',
|
ok: 'OK',
|
||||||
},
|
},
|
||||||
|
list: {
|
||||||
|
delete: {
|
||||||
|
title: 'Are you sure Delete?',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
batchAction: {
|
||||||
|
selected: 'Selected',
|
||||||
|
delete: 'Delete',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
},
|
||||||
errorMessage: {
|
errorMessage: {
|
||||||
answerRequired: 'Answer is required',
|
answerRequired: 'Answer is required',
|
||||||
queryRequired: 'Question is required',
|
queryRequired: 'Question is required',
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,16 @@ const translation = {
|
||||||
error: '导入出错',
|
error: '导入出错',
|
||||||
ok: '确定',
|
ok: '确定',
|
||||||
},
|
},
|
||||||
|
list: {
|
||||||
|
delete: {
|
||||||
|
title: '确定删除吗?',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
batchAction: {
|
||||||
|
selected: '已选择',
|
||||||
|
delete: '删除',
|
||||||
|
cancel: '取消',
|
||||||
|
},
|
||||||
errorMessage: {
|
errorMessage: {
|
||||||
answerRequired: '回复不能为空',
|
answerRequired: '回复不能为空',
|
||||||
queryRequired: '提问不能为空',
|
queryRequired: '提问不能为空',
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,11 @@ export const delAnnotation = (appId: string, annotationId: string) => {
|
||||||
return del(`apps/${appId}/annotations/${annotationId}`)
|
return del(`apps/${appId}/annotations/${annotationId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const delAnnotations = (appId: string, annotationIds: string[]) => {
|
||||||
|
const params = annotationIds.map(id => `annotation_id=${id}`).join('&')
|
||||||
|
return del(`/apps/${appId}/annotations?${params}`)
|
||||||
|
}
|
||||||
|
|
||||||
export const fetchHitHistoryList = (appId: string, annotationId: string, params: Record<string, any>) => {
|
export const fetchHitHistoryList = (appId: string, annotationId: string, params: Record<string, any>) => {
|
||||||
return get(`apps/${appId}/annotations/${annotationId}/hit-histories`, { params })
|
return get(`apps/${appId}/annotations/${annotationId}/hit-histories`, { params })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue