feat: annotation management frontend (#1764)
|
@ -0,0 +1,17 @@
|
|||
import React from 'react'
|
||||
import Main from '@/app/components/app/log-annotation'
|
||||
import { PageType } from '@/app/components/app/configuration/toolbox/annotation/type'
|
||||
|
||||
export type IProps = {
|
||||
params: { appId: string }
|
||||
}
|
||||
|
||||
const Logs = async ({
|
||||
params: { appId },
|
||||
}: IProps) => {
|
||||
return (
|
||||
<Main pageType={PageType.annotation} appId={appId} />
|
||||
)
|
||||
}
|
||||
|
||||
export default Logs
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react'
|
||||
import Main from '@/app/components/app/log'
|
||||
import Main from '@/app/components/app/log-annotation'
|
||||
import { PageType } from '@/app/components/app/configuration/toolbox/annotation/type'
|
||||
|
||||
export type IProps = {
|
||||
params: { appId: string }
|
||||
|
@ -9,7 +10,7 @@ const Logs = async ({
|
|||
params: { appId },
|
||||
}: IProps) => {
|
||||
return (
|
||||
<Main appId={appId} />
|
||||
<Main pageType={PageType.log} appId={appId} />
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,15 @@ export default function NavLink({
|
|||
mode = 'expand',
|
||||
}: NavLinkProps) {
|
||||
const segment = useSelectedLayoutSegment()
|
||||
const isActive = href.toLowerCase().split('/')?.pop() === segment?.toLowerCase()
|
||||
const formattedSegment = (() => {
|
||||
let res = segment?.toLowerCase()
|
||||
// logs and annotations use the same nav
|
||||
if (res === 'annotations')
|
||||
res = 'logs'
|
||||
|
||||
return res
|
||||
})()
|
||||
const isActive = href.toLowerCase().split('/')?.pop() === formattedSegment
|
||||
const NavIcon = isActive ? iconMap.selected : iconMap.normal
|
||||
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Textarea from 'rc-textarea'
|
||||
import { Robot, User } from '@/app/components/base/icons/src/public/avatar'
|
||||
|
||||
export enum EditItemType {
|
||||
Query = 'query',
|
||||
Answer = 'answer',
|
||||
}
|
||||
type Props = {
|
||||
type: EditItemType
|
||||
content: string
|
||||
onChange: (content: string) => void
|
||||
}
|
||||
|
||||
const EditItem: FC<Props> = ({
|
||||
type,
|
||||
content,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const avatar = type === EditItemType.Query ? <User className='w-6 h-6' /> : <Robot className='w-6 h-6' />
|
||||
const name = type === EditItemType.Query ? t('appAnnotation.addModal.queryName') : t('appAnnotation.addModal.answerName')
|
||||
const placeholder = type === EditItemType.Query ? t('appAnnotation.addModal.queryPlaceholder') : t('appAnnotation.addModal.answerPlaceholder')
|
||||
|
||||
return (
|
||||
<div className='flex' onClick={e => e.stopPropagation()}>
|
||||
<div className='shrink-0 mr-3'>
|
||||
{avatar}
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<div className='mb-1 leading-[18px] text-xs font-semibold text-gray-900'>{name}</div>
|
||||
<Textarea
|
||||
className='mt-1 block w-full leading-5 max-h-none text-sm text-gray-700 outline-none appearance-none resize-none'
|
||||
value={content}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)}
|
||||
autoSize={{ minRows: 3 }}
|
||||
placeholder={placeholder}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(EditItem)
|
120
web/app/components/app/annotation/add-annotation-modal/index.tsx
Normal file
|
@ -0,0 +1,120 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { AnnotationItemBasic } from '../type'
|
||||
import EditItem, { EditItemType } from './edit-item'
|
||||
import Drawer from '@/app/components/base/drawer-plus'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import AnnotationFull from '@/app/components/billing/annotation-full'
|
||||
type Props = {
|
||||
isShow: boolean
|
||||
onHide: () => void
|
||||
onAdd: (payload: AnnotationItemBasic) => void
|
||||
}
|
||||
|
||||
const AddAnnotationModal: FC<Props> = ({
|
||||
isShow,
|
||||
onHide,
|
||||
onAdd,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const isAnnotationFull = (enableBilling && plan.usage.annotatedResponse >= plan.total.annotatedResponse)
|
||||
const [question, setQuestion] = useState('')
|
||||
const [answer, setAnswer] = useState('')
|
||||
const [isCreateNext, setIsCreateNext] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
const isValid = (payload: AnnotationItemBasic) => {
|
||||
if (!payload.question)
|
||||
return t('appAnnotation.errorMessage.queryRequired')
|
||||
|
||||
if (!payload.answer)
|
||||
return t('appAnnotation.errorMessage.answerRequired')
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
const payload = {
|
||||
question,
|
||||
answer,
|
||||
}
|
||||
if (isValid(payload) !== true) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: isValid(payload) as string,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await onAdd(payload)
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
setIsSaving(false)
|
||||
|
||||
if (isCreateNext) {
|
||||
setQuestion('')
|
||||
setAnswer('')
|
||||
}
|
||||
else {
|
||||
onHide()
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Drawer
|
||||
isShow={isShow}
|
||||
onHide={onHide}
|
||||
maxWidthClassName='!max-w-[480px]'
|
||||
title={t('appAnnotation.addModal.title') as string}
|
||||
body={(
|
||||
<div className='p-6 pb-4 space-y-6'>
|
||||
<EditItem
|
||||
type={EditItemType.Query}
|
||||
content={question}
|
||||
onChange={setQuestion}
|
||||
/>
|
||||
<EditItem
|
||||
type={EditItemType.Answer}
|
||||
content={answer}
|
||||
onChange={setAnswer}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
foot={
|
||||
(
|
||||
<div>
|
||||
{isAnnotationFull && (
|
||||
<div className='mt-6 mb-4 px-6'>
|
||||
<AnnotationFull />
|
||||
</div>
|
||||
)}
|
||||
<div className='px-6 flex h-16 items-center justify-between border-t border-black/5 bg-gray-50 rounded-bl-xl rounded-br-xl leading-[18px] text-[13px] font-medium text-gray-500'>
|
||||
<div
|
||||
className='flex items-center space-x-2'
|
||||
>
|
||||
<input type="checkbox" checked={isCreateNext} onChange={() => setIsCreateNext(!isCreateNext)} className="w-4 h-4 rounded border-gray-300 text-blue-700 focus:ring-blue-700" />
|
||||
<div>{t('appAnnotation.addModal.createNext')}</div>
|
||||
</div>
|
||||
<div className='mt-2 flex space-x-2'>
|
||||
<Button className='!h-7 !text-xs !font-medium' onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
<Button className='!h-7 !text-xs !font-medium' type='primary' onClick={handleSave} loading={isSaving} disabled={isAnnotationFull}>{t('common.operation.add')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
>
|
||||
</Drawer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(AddAnnotationModal)
|
|
@ -0,0 +1,74 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import {
|
||||
useCSVDownloader,
|
||||
} from 'react-papaparse'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { Download02 as DownloadIcon } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import I18n from '@/context/i18n'
|
||||
|
||||
const CSV_TEMPLATE_QA_EN = [
|
||||
['question', 'answer'],
|
||||
['question1', 'answer1'],
|
||||
['question2', 'answer2'],
|
||||
]
|
||||
const CSV_TEMPLATE_QA_CN = [
|
||||
['问题', '答案'],
|
||||
['问题 1', '答案 1'],
|
||||
['问题 2', '答案 2'],
|
||||
]
|
||||
|
||||
const CSVDownload: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const { CSVDownloader, Type } = useCSVDownloader()
|
||||
|
||||
const getTemplate = () => {
|
||||
if (locale === 'en')
|
||||
return CSV_TEMPLATE_QA_EN
|
||||
|
||||
return CSV_TEMPLATE_QA_CN
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mt-6'>
|
||||
<div className='text-sm text-gray-900 font-medium'>{t('share.generation.csvStructureTitle')}</div>
|
||||
<div className='mt-2 max-h-[500px] overflow-auto'>
|
||||
<table className='table-fixed w-full border-separate border-spacing-0 border border-gray-200 rounded-lg text-xs'>
|
||||
<thead className='text-gray-500'>
|
||||
<tr>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-gray-200'>{t('appAnnotation.batchModal.question')}</td>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-gray-200'>{t('appAnnotation.batchModal.answer')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className='text-gray-700'>
|
||||
<tr>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-gray-100 text-[13px]'>{t('appAnnotation.batchModal.question')} 1</td>
|
||||
<td className='h-9 pl-3 pr-2 border-b border-gray-100 text-[13px]'>{t('appAnnotation.batchModal.answer')} 1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className='h-9 pl-3 pr-2 text-[13px]'>{t('appAnnotation.batchModal.question')} 2</td>
|
||||
<td className='h-9 pl-3 pr-2 text-[13px]'>{t('appAnnotation.batchModal.answer')} 2</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<CSVDownloader
|
||||
className="block mt-2 cursor-pointer"
|
||||
type={Type.Link}
|
||||
filename={'template'}
|
||||
bom={true}
|
||||
data={getTemplate()}
|
||||
>
|
||||
<div className='flex items-center h-[18px] space-x-1 text-[#155EEF] text-xs font-medium'>
|
||||
<DownloadIcon className='w-3 h-3 mr-1' />
|
||||
{t('appAnnotation.batchModal.template')}
|
||||
</div>
|
||||
</CSVDownloader>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(CSVDownload)
|
|
@ -0,0 +1,126 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { Trash03 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
export type Props = {
|
||||
file: File | undefined
|
||||
updateFile: (file?: File) => void
|
||||
}
|
||||
|
||||
const CSVUploader: FC<Props> = ({
|
||||
file,
|
||||
updateFile,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const dropRef = useRef<HTMLDivElement>(null)
|
||||
const dragRef = useRef<HTMLDivElement>(null)
|
||||
const fileUploader = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target !== dragRef.current && setDragging(true)
|
||||
}
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.target === dragRef.current && setDragging(false)
|
||||
}
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setDragging(false)
|
||||
if (!e.dataTransfer)
|
||||
return
|
||||
const files = [...e.dataTransfer.files]
|
||||
if (files.length > 1) {
|
||||
notify({ type: 'error', message: t('datasetCreation.stepOne.uploader.validation.count') })
|
||||
return
|
||||
}
|
||||
updateFile(files[0])
|
||||
}
|
||||
const selectHandle = () => {
|
||||
if (fileUploader.current)
|
||||
fileUploader.current.click()
|
||||
}
|
||||
const removeFile = () => {
|
||||
if (fileUploader.current)
|
||||
fileUploader.current.value = ''
|
||||
updateFile()
|
||||
}
|
||||
const fileChangeHandle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const currentFile = e.target.files?.[0]
|
||||
updateFile(currentFile)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dropRef.current?.addEventListener('dragenter', handleDragEnter)
|
||||
dropRef.current?.addEventListener('dragover', handleDragOver)
|
||||
dropRef.current?.addEventListener('dragleave', handleDragLeave)
|
||||
dropRef.current?.addEventListener('drop', handleDrop)
|
||||
return () => {
|
||||
dropRef.current?.removeEventListener('dragenter', handleDragEnter)
|
||||
dropRef.current?.removeEventListener('dragover', handleDragOver)
|
||||
dropRef.current?.removeEventListener('dragleave', handleDragLeave)
|
||||
dropRef.current?.removeEventListener('drop', handleDrop)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className='mt-6'>
|
||||
<input
|
||||
ref={fileUploader}
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
id="fileUploader"
|
||||
accept='.csv'
|
||||
onChange={fileChangeHandle}
|
||||
/>
|
||||
<div ref={dropRef}>
|
||||
{!file && (
|
||||
<div className={cn('flex items-center h-20 rounded-xl bg-gray-50 border border-dashed border-gray-200 text-sm font-normal', dragging && 'bg-[#F5F8FF] border border-[#B2CCFF]')}>
|
||||
<div className='w-full flex items-center justify-center space-x-2'>
|
||||
<CSVIcon className="shrink-0" />
|
||||
<div className='text-gray-500'>
|
||||
{t('appAnnotation.batchModal.csvUploadTitle')}
|
||||
<span className='text-primary-400 cursor-pointer' onClick={selectHandle}>{t('appAnnotation.batchModal.browse')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{dragging && <div ref={dragRef} className='absolute w-full h-full top-0 left-0' />}
|
||||
</div>
|
||||
)}
|
||||
{file && (
|
||||
<div className={cn('flex items-center h-20 px-6 rounded-xl bg-gray-50 border border-gray-200 text-sm font-normal group', 'hover:bg-[#F5F8FF] hover:border-[#B2CCFF]')}>
|
||||
<CSVIcon className="shrink-0" />
|
||||
<div className='flex ml-2 w-0 grow'>
|
||||
<span className='max-w-[calc(100%_-_30px)] text-ellipsis whitespace-nowrap overflow-hidden text-gray-800'>{file.name.replace(/.csv$/, '')}</span>
|
||||
<span className='shrink-0 text-gray-500'>.csv</span>
|
||||
</div>
|
||||
<div className='hidden group-hover:flex items-center'>
|
||||
<Button className='!h-8 !px-3 !py-[6px] bg-white !text-[13px] !leading-[18px] text-gray-700' onClick={selectHandle}>{t('datasetCreation.stepOne.uploader.change')}</Button>
|
||||
<div className='mx-2 w-px h-4 bg-gray-200' />
|
||||
<div className='p-2 cursor-pointer' onClick={removeFile}>
|
||||
<Trash03 className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(CSVUploader)
|
|
@ -0,0 +1,124 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import CSVUploader from './csv-uploader'
|
||||
import CSVDownloader from './csv-downloader'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { annotationBatchImport, checkAnnotationBatchImportProgress } from '@/service/annotation'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import AnnotationFull from '@/app/components/billing/annotation-full'
|
||||
|
||||
export enum ProcessStatus {
|
||||
WAITING = 'waiting',
|
||||
PROCESSING = 'processing',
|
||||
COMPLETED = 'completed',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
export type IBatchModalProps = {
|
||||
appId: string
|
||||
isShow: boolean
|
||||
onCancel: () => void
|
||||
onAdded: () => void
|
||||
}
|
||||
|
||||
const BatchModal: FC<IBatchModalProps> = ({
|
||||
appId,
|
||||
isShow,
|
||||
onCancel,
|
||||
onAdded,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const isAnnotationFull = (enableBilling && plan.usage.annotatedResponse >= plan.total.annotatedResponse)
|
||||
const [currentCSV, setCurrentCSV] = useState<File>()
|
||||
const handleFile = (file?: File) => setCurrentCSV(file)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isShow)
|
||||
setCurrentCSV(undefined)
|
||||
}, [isShow])
|
||||
|
||||
const [importStatus, setImportStatus] = useState<ProcessStatus | string>()
|
||||
const notify = Toast.notify
|
||||
const checkProcess = async (jobID: string) => {
|
||||
try {
|
||||
const res = await checkAnnotationBatchImportProgress({ jobID, appId })
|
||||
setImportStatus(res.job_status)
|
||||
if (res.job_status === ProcessStatus.WAITING || res.job_status === ProcessStatus.PROCESSING)
|
||||
setTimeout(() => checkProcess(res.job_id), 2500)
|
||||
if (res.job_status === ProcessStatus.ERROR)
|
||||
notify({ type: 'error', message: `${t('appAnnotation.batchModal.runError')}` })
|
||||
if (res.job_status === ProcessStatus.COMPLETED) {
|
||||
notify({ type: 'success', message: `${t('appAnnotation.batchModal.completed')}` })
|
||||
onAdded()
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: `${t('appAnnotation.batchModal.runError')}${'message' in e ? `: ${e.message}` : ''}` })
|
||||
}
|
||||
}
|
||||
|
||||
const runBatch = async (csv: File) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', csv)
|
||||
try {
|
||||
const res = await annotationBatchImport({
|
||||
url: `/apps/${appId}/annotations/batch-import`,
|
||||
body: formData,
|
||||
})
|
||||
setImportStatus(res.job_status)
|
||||
checkProcess(res.job_id)
|
||||
}
|
||||
catch (e: any) {
|
||||
notify({ type: 'error', message: `${t('appAnnotation.batchModal.runError')}${'message' in e ? `: ${e.message}` : ''}` })
|
||||
}
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
if (!currentCSV)
|
||||
return
|
||||
runBatch(currentCSV)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isShow={isShow} onClose={() => { }} wrapperClassName='!z-[20]' className='px-8 py-6 !max-w-[520px] !rounded-xl'>
|
||||
<div className='relative pb-1 text-xl font-medium leading-[30px] text-gray-900'>{t('appAnnotation.batchModal.title')}</div>
|
||||
<div className='absolute right-4 top-4 p-2 cursor-pointer' onClick={onCancel}>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
<CSVUploader
|
||||
file={currentCSV}
|
||||
updateFile={handleFile}
|
||||
/>
|
||||
<CSVDownloader />
|
||||
|
||||
{isAnnotationFull && (
|
||||
<div className='mt-4'>
|
||||
<AnnotationFull />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='mt-[28px] pt-6 flex justify-end'>
|
||||
<Button className='mr-2 text-gray-700 text-sm font-medium' onClick={onCancel}>
|
||||
{t('appAnnotation.batchModal.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
className='text-sm font-medium'
|
||||
type="primary"
|
||||
onClick={handleSend}
|
||||
disabled={isAnnotationFull || !currentCSV}
|
||||
loading={importStatus === ProcessStatus.PROCESSING}
|
||||
>
|
||||
{t('appAnnotation.batchModal.run')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(BatchModal)
|
|
@ -0,0 +1,131 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Textarea from 'rc-textarea'
|
||||
import cn from 'classnames'
|
||||
import { Robot, User } from '@/app/components/base/icons/src/public/avatar'
|
||||
import { Edit04, Trash03 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Edit04 as EditSolid } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
export enum EditItemType {
|
||||
Query = 'query',
|
||||
Answer = 'answer',
|
||||
}
|
||||
type Props = {
|
||||
type: EditItemType
|
||||
content: string
|
||||
readonly?: boolean
|
||||
onSave: (content: string) => void
|
||||
}
|
||||
|
||||
export const EditTitle: FC<{ className?: string; title: string }> = ({ className, title }) => (
|
||||
<div className={cn(className, 'flex items-center height-[18px] text-xs font-medium text-gray-500')}>
|
||||
<EditSolid className='mr-1 w-3.5 h-3.5' />
|
||||
<div>{title}</div>
|
||||
<div
|
||||
className='ml-2 grow h-[1px]'
|
||||
style={{
|
||||
background: 'linear-gradient(90deg, rgba(0, 0, 0, 0.05) -1.65%, rgba(0, 0, 0, 0.00) 100%)',
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
const EditItem: FC<Props> = ({
|
||||
type,
|
||||
readonly,
|
||||
content,
|
||||
onSave,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [newContent, setNewContent] = useState('')
|
||||
const showNewContent = newContent && newContent !== content
|
||||
const avatar = type === EditItemType.Query ? <User className='w-6 h-6' /> : <Robot className='w-6 h-6' />
|
||||
const name = type === EditItemType.Query ? t('appAnnotation.editModal.queryName') : t('appAnnotation.editModal.answerName')
|
||||
const editTitle = type === EditItemType.Query ? t('appAnnotation.editModal.yourQuery') : t('appAnnotation.editModal.yourAnswer')
|
||||
const placeholder = type === EditItemType.Query ? t('appAnnotation.editModal.queryPlaceholder') : t('appAnnotation.editModal.answerPlaceholder')
|
||||
const [isEdit, setIsEdit] = useState(false)
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(newContent)
|
||||
setIsEdit(false)
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setNewContent('')
|
||||
setIsEdit(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex' onClick={e => e.stopPropagation()}>
|
||||
<div className='shrink-0 mr-3'>
|
||||
{avatar}
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<div className='mb-1 leading-[18px] text-xs font-semibold text-gray-900'>{name}</div>
|
||||
<div className='leading-5 text-sm font-normal text-gray-900'>{content}</div>
|
||||
{!isEdit
|
||||
? (
|
||||
<div>
|
||||
{showNewContent && (
|
||||
<div className='mt-3'>
|
||||
<EditTitle title={editTitle} />
|
||||
<div className='mt-1 leading-5 text-sm font-normal text-gray-900'>{newContent}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='mt-2 flex items-center'>
|
||||
{!readonly && (
|
||||
<div
|
||||
className='flex items-center space-x-1 leading-[18px] text-xs font-medium text-[#155EEF] cursor-pointer'
|
||||
onClick={(e) => {
|
||||
setIsEdit(true)
|
||||
}}
|
||||
>
|
||||
<Edit04 className='mr-1 w-3.5 h-3.5' />
|
||||
<div>{t('common.operation.edit')}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showNewContent && (
|
||||
<div className='ml-2 flex items-center leading-[18px] text-xs font-medium text-gray-500'>
|
||||
<div className='mr-2'>·</div>
|
||||
<div
|
||||
className='flex items-center space-x-1 cursor-pointer'
|
||||
onClick={() => {
|
||||
setNewContent(content)
|
||||
onSave(content)
|
||||
}}
|
||||
>
|
||||
<div className='w-3.5 h-3.5'>
|
||||
<Trash03 className='w-3.5 h-3.5' />
|
||||
</div>
|
||||
<div>{t('common.operation.delete')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className='mt-3'>
|
||||
<EditTitle title={editTitle} />
|
||||
<Textarea
|
||||
className='mt-1 block w-full leading-5 max-h-none text-sm text-gray-700 outline-none appearance-none resize-none'
|
||||
value={newContent}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setNewContent(e.target.value)}
|
||||
autoSize={{ minRows: 3 }}
|
||||
placeholder={placeholder}
|
||||
autoFocus
|
||||
/>
|
||||
<div className='mt-2 flex space-x-2'>
|
||||
<Button className='!h-7 !text-xs !font-medium' type='primary' onClick={handleSave}>{t('common.operation.save')}</Button>
|
||||
<Button className='!h-7 !text-xs !font-medium' onClick={handleCancel}>{t('common.operation.cancel')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(EditItem)
|
|
@ -0,0 +1,142 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import dayjs from 'dayjs'
|
||||
import EditItem, { EditItemType } from './edit-item'
|
||||
import Drawer from '@/app/components/base/drawer-plus'
|
||||
import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication'
|
||||
import DeleteConfirmModal from '@/app/components/base/modal/delete-confirm-modal'
|
||||
import { addAnnotation, editAnnotation } from '@/service/annotation'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import AnnotationFull from '@/app/components/billing/annotation-full'
|
||||
type Props = {
|
||||
isShow: boolean
|
||||
onHide: () => void
|
||||
appId: string
|
||||
messageId?: string
|
||||
annotationId?: string
|
||||
query: string
|
||||
answer: string
|
||||
onEdited: (editedQuery: string, editedAnswer: string) => void
|
||||
onAdded: (annotationId: string, authorName: string, editedQuery: string, editedAnswer: string) => void
|
||||
createdAt?: number
|
||||
onRemove: () => void
|
||||
onlyEditResponse?: boolean
|
||||
}
|
||||
|
||||
const EditAnnotationModal: FC<Props> = ({
|
||||
isShow,
|
||||
onHide,
|
||||
query,
|
||||
answer,
|
||||
onEdited,
|
||||
onAdded,
|
||||
appId,
|
||||
messageId,
|
||||
annotationId,
|
||||
createdAt,
|
||||
onRemove,
|
||||
onlyEditResponse,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const isAdd = !annotationId
|
||||
const isAnnotationFull = (enableBilling && plan.usage.annotatedResponse >= plan.total.annotatedResponse)
|
||||
const handleSave = async (type: EditItemType, editedContent: string) => {
|
||||
let postQuery = query
|
||||
let postAnswer = answer
|
||||
if (type === EditItemType.Query)
|
||||
postQuery = editedContent
|
||||
else
|
||||
postAnswer = editedContent
|
||||
if (!isAdd) {
|
||||
await editAnnotation(appId, annotationId, {
|
||||
message_id: messageId,
|
||||
question: postQuery,
|
||||
answer: postAnswer,
|
||||
})
|
||||
onEdited(postQuery, postAnswer)
|
||||
}
|
||||
else {
|
||||
const res: any = await addAnnotation(appId, {
|
||||
question: postQuery,
|
||||
answer: postAnswer,
|
||||
message_id: messageId,
|
||||
})
|
||||
onAdded(res.id, res.account?.name, postQuery, postAnswer)
|
||||
}
|
||||
|
||||
Toast.notify({
|
||||
message: t('common.api.actionSuccess') as string,
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Drawer
|
||||
isShow={isShow}
|
||||
onHide={onHide}
|
||||
maxWidthClassName='!max-w-[480px]'
|
||||
title={t('appAnnotation.editModal.title') as string}
|
||||
body={(
|
||||
<div className='p-6 pb-4 space-y-6'>
|
||||
<EditItem
|
||||
type={EditItemType.Query}
|
||||
content={query}
|
||||
readonly={(isAdd && isAnnotationFull) || onlyEditResponse}
|
||||
onSave={editedContent => handleSave(EditItemType.Query, editedContent)}
|
||||
/>
|
||||
<EditItem
|
||||
type={EditItemType.Answer}
|
||||
content={answer}
|
||||
readonly={isAdd && isAnnotationFull}
|
||||
onSave={editedContent => handleSave(EditItemType.Answer, editedContent)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
foot={
|
||||
<div>
|
||||
{isAnnotationFull && (
|
||||
<div className='mt-6 mb-4 px-6'>
|
||||
<AnnotationFull />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{
|
||||
annotationId
|
||||
? (
|
||||
<div className='px-4 flex h-16 items-center justify-between border-t border-black/5 bg-gray-50 rounded-bl-xl rounded-br-xl leading-[18px] text-[13px] font-medium text-gray-500'>
|
||||
<div
|
||||
className='flex items-center pl-3 space-x-2 cursor-pointer'
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
<MessageCheckRemove />
|
||||
<div>{t('appAnnotation.editModal.removeThisCache')}</div>
|
||||
</div>
|
||||
{createdAt && <div>{t('appAnnotation.editModal.createdAt')} {dayjs(createdAt * 1000).format('YYYY-MM-DD hh:mm')}</div>}
|
||||
</div>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
</Drawer>
|
||||
<DeleteConfirmModal
|
||||
isShow={showModal}
|
||||
onHide={() => setShowModal(false)}
|
||||
onRemove={() => {
|
||||
onRemove()
|
||||
setShowModal(false)
|
||||
}}
|
||||
text={t('appDebug.feature.annotation.removeConfirm') as string}
|
||||
/>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(EditAnnotationModal)
|
26
web/app/components/app/annotation/empty-element.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
'use client'
|
||||
import type { FC, SVGProps } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const ThreeDotsIcon = ({ className }: SVGProps<SVGElement>) => {
|
||||
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
||||
<path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
const EmptyElement: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-center h-full'>
|
||||
<div className='bg-gray-50 w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
|
||||
<span className='text-gray-700 font-semibold'>{t('appAnnotation.noData.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
|
||||
<div className='mt-2 text-gray-500 text-sm font-normal'>
|
||||
{t('appAnnotation.noData.description')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(EmptyElement)
|
54
web/app/components/app/annotation/filter.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
MagnifyingGlassIcon,
|
||||
} from '@heroicons/react/24/solid'
|
||||
import useSWR from 'swr'
|
||||
import { fetchAnnotationsCount } from '@/service/log'
|
||||
|
||||
export type QueryParam = {
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
type IFilterProps = {
|
||||
appId: string
|
||||
queryParams: QueryParam
|
||||
setQueryParams: (v: QueryParam) => void
|
||||
children: JSX.Element
|
||||
}
|
||||
|
||||
const Filter: FC<IFilterProps> = ({
|
||||
appId,
|
||||
queryParams,
|
||||
setQueryParams,
|
||||
children,
|
||||
}) => {
|
||||
// TODO: change fetch list api
|
||||
const { data } = useSWR({ url: `/apps/${appId}/annotations/count` }, fetchAnnotationsCount)
|
||||
const { t } = useTranslation()
|
||||
if (!data)
|
||||
return null
|
||||
return (
|
||||
<div className='flex justify-between flex-row flex-wrap gap-y-2 gap-x-4 items-center mb-4 text-gray-900 text-base'>
|
||||
<div className="relative">
|
||||
<div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="query"
|
||||
className="block w-[240px] bg-gray-100 shadow-sm rounded-md border-0 py-1.5 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-6"
|
||||
placeholder={t('common.operation.search') as string}
|
||||
value={queryParams.keyword}
|
||||
onChange={(e) => {
|
||||
setQueryParams({ ...queryParams, keyword: e.target.value })
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Filter)
|
141
web/app/components/app/annotation/header-opts/index.tsx
Normal file
|
@ -0,0 +1,141 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import {
|
||||
useCSVDownloader,
|
||||
} from 'react-papaparse'
|
||||
import Button from '../../../base/button'
|
||||
import { Plus } from '../../../base/icons/src/vender/line/general'
|
||||
import AddAnnotationModal from '../add-annotation-modal'
|
||||
import type { AnnotationItemBasic } from '../type'
|
||||
import BatchAddModal from '../batch-add-annotation-modal'
|
||||
import s from './style.module.css'
|
||||
import CustomPopover from '@/app/components/base/popover'
|
||||
// import Divider from '@/app/components/base/divider'
|
||||
import { FileDownload02, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import I18n from '@/context/i18n'
|
||||
import { fetchExportAnnotationList } from '@/service/annotation'
|
||||
const CSV_HEADER_QA_EN = ['Question', 'Answer']
|
||||
const CSV_HEADER_QA_CN = ['问题', '答案']
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
onAdd: (payload: AnnotationItemBasic) => void
|
||||
onAdded: () => void
|
||||
controlUpdateList: number
|
||||
// onClearAll: () => void
|
||||
}
|
||||
|
||||
const HeaderOptions: FC<Props> = ({
|
||||
appId,
|
||||
onAdd,
|
||||
onAdded,
|
||||
// onClearAll,
|
||||
controlUpdateList,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const { CSVDownloader, Type } = useCSVDownloader()
|
||||
const [list, setList] = useState<AnnotationItemBasic[]>([])
|
||||
const fetchList = async () => {
|
||||
const { data }: any = await fetchExportAnnotationList(appId)
|
||||
setList(data as AnnotationItemBasic[])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchList()
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (controlUpdateList)
|
||||
fetchList()
|
||||
}, [controlUpdateList])
|
||||
|
||||
const [showBulkImportModal, setShowBulkImportModal] = useState(false)
|
||||
|
||||
const Operations = () => {
|
||||
return (
|
||||
<div className="w-full py-1">
|
||||
<button className={s.actionItem} onClick={() => {
|
||||
setShowBulkImportModal(true)
|
||||
}}>
|
||||
<FilePlus02 className={s.actionItemIcon} />
|
||||
<span className={s.actionName}>{t('appAnnotation.table.header.bulkImport')}</span>
|
||||
</button>
|
||||
|
||||
<CSVDownloader
|
||||
type={Type.Link}
|
||||
filename="annotations"
|
||||
bom={true}
|
||||
data={[
|
||||
locale === 'en' ? CSV_HEADER_QA_EN : CSV_HEADER_QA_CN,
|
||||
...list.map(item => [item.question, item.answer]),
|
||||
]}
|
||||
>
|
||||
<button className={s.actionItem}>
|
||||
<FileDownload02 className={s.actionItemIcon} />
|
||||
<span className={s.actionName}>{t('appAnnotation.table.header.bulkExport')}</span>
|
||||
</button>
|
||||
</CSVDownloader>
|
||||
|
||||
{/* <Divider className="!my-1" />
|
||||
<div
|
||||
className={cn(s.actionItem, s.deleteActionItem, 'group')}
|
||||
onClick={onClickDelete}
|
||||
>
|
||||
<Trash03 className={cn(s.actionItemIcon, 'group-hover:text-red-500')} />
|
||||
<span className={cn(s.actionName, 'group-hover:text-red-500')}>
|
||||
{t('appAnnotation.table.header.clearAll')}
|
||||
</span>
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const [showAddModal, setShowAddModal] = React.useState(false)
|
||||
|
||||
return (
|
||||
<div className='flex space-x-2'>
|
||||
<Button type='primary' onClick={() => setShowAddModal(true)} className='flex items-center !h-8 !px-3 !text-[13px] space-x-2'>
|
||||
<Plus className='w-4 h-4' />
|
||||
<div>{t('appAnnotation.table.header.addAnnotation')}</div>
|
||||
</Button>
|
||||
<CustomPopover
|
||||
htmlContent={<Operations />}
|
||||
position="br"
|
||||
trigger="click"
|
||||
btnElement={<div className={cn(s.actionIcon, s.commonIcon)} />}
|
||||
btnClassName={open =>
|
||||
cn(
|
||||
open ? 'border-gray-300 !bg-gray-100 !shadow-none' : 'border-gray-200',
|
||||
s.actionIconWrapper,
|
||||
)
|
||||
}
|
||||
// !w-[208px]
|
||||
className={'!w-[131px] h-fit !z-20'}
|
||||
manualClose
|
||||
/>
|
||||
{showAddModal && (
|
||||
<AddAnnotationModal
|
||||
isShow={showAddModal}
|
||||
onHide={() => setShowAddModal(false)}
|
||||
onAdd={onAdd}
|
||||
/>
|
||||
)}
|
||||
|
||||
{
|
||||
showBulkImportModal && (
|
||||
<BatchAddModal
|
||||
appId={appId}
|
||||
isShow={showBulkImportModal}
|
||||
onCancel={() => setShowBulkImportModal(false)}
|
||||
onAdded={onAdded}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(HeaderOptions)
|
|
@ -0,0 +1,32 @@
|
|||
.actionIconWrapper {
|
||||
@apply h-8 w-8 p-2 rounded-md hover:bg-gray-100 !important;
|
||||
}
|
||||
|
||||
.commonIcon {
|
||||
@apply w-4 h-4 inline-block align-middle;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.actionIcon {
|
||||
@apply bg-gray-500;
|
||||
mask-image: url(~@/assets/action.svg);
|
||||
}
|
||||
|
||||
.actionItemIcon {
|
||||
@apply w-4 h-4 text-gray-500;
|
||||
}
|
||||
|
||||
.actionItem {
|
||||
@apply h-9 py-2 px-3 mx-1 flex items-center space-x-2 hover:bg-gray-100 rounded-lg cursor-pointer;
|
||||
width: calc(100% - 0.5rem);
|
||||
}
|
||||
|
||||
.deleteActionItem {
|
||||
@apply hover:bg-red-50 !important;
|
||||
}
|
||||
|
||||
.actionName {
|
||||
@apply text-gray-700 text-sm;
|
||||
}
|
315
web/app/components/app/annotation/index.tsx
Normal file
|
@ -0,0 +1,315 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Pagination } from 'react-headless-pagination'
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline'
|
||||
import cn from 'classnames'
|
||||
import Toast from '../../base/toast'
|
||||
import Filter from './filter'
|
||||
import type { QueryParam } from './filter'
|
||||
import List from './list'
|
||||
import EmptyElement from './empty-element'
|
||||
import HeaderOpts from './header-opts'
|
||||
import s from './style.module.css'
|
||||
import { AnnotationEnableStatus, type AnnotationItem, type AnnotationItemBasic, JobStatus } from './type'
|
||||
import ViewAnnotationModal from './view-annotation-modal'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import { addAnnotation, delAnnotation, fetchAnnotationConfig as doFetchAnnotationConfig, editAnnotation, fetchAnnotationList, queryAnnotationJobStatus, updateAnnotationScore, updateAnnotationStatus } from '@/service/annotation'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { APP_PAGE_LIMIT } from '@/config'
|
||||
import ConfigParamModal from '@/app/components/app/configuration/toolbox/annotation/config-param-modal'
|
||||
import type { AnnotationReplyConfig } from '@/models/debug'
|
||||
import { sleep } from '@/utils'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
|
||||
import { Settings04 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { fetchAppDetail } from '@/service/apps'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
}
|
||||
|
||||
const Annotation: FC<Props> = ({
|
||||
appId,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isShowEdit, setIsShowEdit] = React.useState(false)
|
||||
const [annotationConfig, setAnnotationConfig] = useState<AnnotationReplyConfig | null>(null)
|
||||
const [isChatApp, setIsChatApp] = useState(false)
|
||||
|
||||
const fetchAnnotationConfig = async () => {
|
||||
const res = await doFetchAnnotationConfig(appId)
|
||||
setAnnotationConfig(res as AnnotationReplyConfig)
|
||||
}
|
||||
useEffect(() => {
|
||||
fetchAppDetail({ url: '/apps', id: appId }).then(async (res: any) => {
|
||||
const isChatApp = res.mode === 'chat'
|
||||
setIsChatApp(isChatApp)
|
||||
if (isChatApp)
|
||||
fetchAnnotationConfig()
|
||||
})
|
||||
}, [])
|
||||
const [controlRefreshSwitch, setControlRefreshSwitch] = useState(Date.now())
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const isAnnotationFull = (enableBilling && plan.usage.annotatedResponse >= plan.total.annotatedResponse)
|
||||
const [isShowAnnotationFullModal, setIsShowAnnotationFullModal] = useState(false)
|
||||
const ensureJobCompleted = async (jobId: string, status: AnnotationEnableStatus) => {
|
||||
let isCompleted = false
|
||||
while (!isCompleted) {
|
||||
const res: any = await queryAnnotationJobStatus(appId, status, jobId)
|
||||
isCompleted = res.job_status === JobStatus.completed
|
||||
if (isCompleted)
|
||||
break
|
||||
|
||||
await sleep(2000)
|
||||
}
|
||||
}
|
||||
|
||||
const [queryParams, setQueryParams] = useState<QueryParam>({})
|
||||
const [currPage, setCurrPage] = React.useState<number>(0)
|
||||
const query = {
|
||||
page: currPage + 1,
|
||||
limit: APP_PAGE_LIMIT,
|
||||
keyword: queryParams.keyword || '',
|
||||
}
|
||||
|
||||
const [controlUpdateList, setControlUpdateList] = useState(Date.now())
|
||||
const [list, setList] = useState<AnnotationItem[]>([])
|
||||
const [total, setTotal] = useState(10)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const fetchList = async (page = 1) => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const { data, total }: any = await fetchAnnotationList(appId, {
|
||||
...query,
|
||||
page,
|
||||
})
|
||||
setList(data as AnnotationItem[])
|
||||
setTotal(total)
|
||||
}
|
||||
catch (e) {
|
||||
|
||||
}
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchList(currPage + 1)
|
||||
}, [currPage])
|
||||
|
||||
useEffect(() => {
|
||||
fetchList(1)
|
||||
setControlUpdateList(Date.now())
|
||||
}, [queryParams])
|
||||
|
||||
const handleAdd = async (payload: AnnotationItemBasic) => {
|
||||
await addAnnotation(appId, {
|
||||
...payload,
|
||||
})
|
||||
Toast.notify({
|
||||
message: t('common.api.actionSuccess'),
|
||||
type: 'success',
|
||||
})
|
||||
fetchList()
|
||||
setControlUpdateList(Date.now())
|
||||
}
|
||||
|
||||
const handleRemove = async (id: string) => {
|
||||
await delAnnotation(appId, id)
|
||||
Toast.notify({
|
||||
message: t('common.api.actionSuccess'),
|
||||
type: 'success',
|
||||
})
|
||||
fetchList()
|
||||
setControlUpdateList(Date.now())
|
||||
}
|
||||
|
||||
const [currItem, setCurrItem] = useState<AnnotationItem | null>(list[0])
|
||||
const [isShowViewModal, setIsShowViewModal] = useState(false)
|
||||
useEffect(() => {
|
||||
if (!isShowEdit)
|
||||
setControlRefreshSwitch(Date.now())
|
||||
}, [isShowEdit])
|
||||
const handleView = (item: AnnotationItem) => {
|
||||
setCurrItem(item)
|
||||
setIsShowViewModal(true)
|
||||
}
|
||||
|
||||
const handleSave = async (question: string, answer: string) => {
|
||||
await editAnnotation(appId, (currItem as AnnotationItem).id, {
|
||||
question,
|
||||
answer,
|
||||
})
|
||||
Toast.notify({
|
||||
message: t('common.api.actionSuccess'),
|
||||
type: 'success',
|
||||
})
|
||||
fetchList()
|
||||
setControlUpdateList(Date.now())
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex flex-col h-full'>
|
||||
<p className='flex text-sm font-normal text-gray-500'>{t('appLog.description')}</p>
|
||||
<div className='grow flex flex-col py-4 '>
|
||||
<Filter appId={appId} queryParams={queryParams} setQueryParams={setQueryParams}>
|
||||
<div className='flex items-center space-x-2'>
|
||||
{isChatApp && (
|
||||
<>
|
||||
<div className={cn(!annotationConfig?.enabled && 'pr-2', 'flex items-center h-7 rounded-lg border border-gray-200 pl-2 space-x-1')}>
|
||||
<div className='leading-[18px] text-[13px] font-medium text-gray-900'>{t('appAnnotation.name')}</div>
|
||||
<Switch
|
||||
key={controlRefreshSwitch}
|
||||
defaultValue={annotationConfig?.enabled}
|
||||
size='md'
|
||||
onChange={async (value) => {
|
||||
if (value) {
|
||||
if (isAnnotationFull) {
|
||||
setIsShowAnnotationFullModal(true)
|
||||
setControlRefreshSwitch(Date.now())
|
||||
return
|
||||
}
|
||||
setIsShowEdit(true)
|
||||
}
|
||||
else {
|
||||
const { job_id: jobId }: any = await updateAnnotationStatus(appId, AnnotationEnableStatus.disable, annotationConfig?.embedding_model, annotationConfig?.score_threshold)
|
||||
await ensureJobCompleted(jobId, AnnotationEnableStatus.disable)
|
||||
await fetchAnnotationConfig()
|
||||
Toast.notify({
|
||||
message: t('common.api.actionSuccess'),
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
}}
|
||||
></Switch>
|
||||
{annotationConfig?.enabled && (
|
||||
<div className='flex items-center pl-1.5'>
|
||||
<div className='shrink-0 mr-1 w-[1px] h-3.5 bg-gray-200'></div>
|
||||
<div
|
||||
className={`
|
||||
shrink-0 h-7 w-7 flex items-center justify-center
|
||||
text-xs text-gray-700 font-medium
|
||||
`}
|
||||
onClick={() => { setIsShowEdit(true) }}
|
||||
>
|
||||
<div className='flex h-6 w-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-200'>
|
||||
<Settings04 className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='shrink-0 mx-3 w-[1px] h-3.5 bg-gray-200'></div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<HeaderOpts
|
||||
appId={appId}
|
||||
controlUpdateList={controlUpdateList}
|
||||
onAdd={handleAdd}
|
||||
onAdded={() => {
|
||||
fetchList()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Filter>
|
||||
{isLoading
|
||||
? <Loading type='app' />
|
||||
: total > 0
|
||||
? <List
|
||||
list={list}
|
||||
onRemove={handleRemove}
|
||||
onView={handleView}
|
||||
/>
|
||||
: <div className='grow flex h-full items-center justify-center'><EmptyElement /></div>
|
||||
}
|
||||
{/* Show Pagination only if the total is more than the limit */}
|
||||
{(total && total > APP_PAGE_LIMIT)
|
||||
? <Pagination
|
||||
className="flex items-center w-full h-10 text-sm select-none mt-8"
|
||||
currentPage={currPage}
|
||||
edgePageCount={2}
|
||||
middlePagesSiblingCount={1}
|
||||
setCurrentPage={setCurrPage}
|
||||
totalPages={Math.ceil(total / APP_PAGE_LIMIT)}
|
||||
truncableClassName="w-8 px-0.5 text-center"
|
||||
truncableText="..."
|
||||
>
|
||||
<Pagination.PrevButton
|
||||
disabled={currPage === 0}
|
||||
className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === 0 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} >
|
||||
<ArrowLeftIcon className="mr-3 h-3 w-3" />
|
||||
{t('appLog.table.pagination.previous')}
|
||||
</Pagination.PrevButton>
|
||||
<div className={`flex items-center justify-center flex-grow ${s.pagination}`}>
|
||||
<Pagination.PageButton
|
||||
activeClassName="bg-primary-50 dark:bg-opacity-0 text-primary-600 dark:text-white"
|
||||
className="flex items-center justify-center h-8 w-8 rounded-full cursor-pointer"
|
||||
inactiveClassName="text-gray-500"
|
||||
/>
|
||||
</div>
|
||||
<Pagination.NextButton
|
||||
disabled={currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1}
|
||||
className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} >
|
||||
{t('appLog.table.pagination.next')}
|
||||
<ArrowRightIcon className="ml-3 h-3 w-3" />
|
||||
</Pagination.NextButton>
|
||||
</Pagination>
|
||||
: null}
|
||||
|
||||
{isShowViewModal && (
|
||||
<ViewAnnotationModal
|
||||
appId={appId}
|
||||
isShow={isShowViewModal}
|
||||
onHide={() => setIsShowViewModal(false)}
|
||||
onRemove={async () => {
|
||||
await handleRemove((currItem as AnnotationItem)?.id)
|
||||
}}
|
||||
item={currItem as AnnotationItem}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)}
|
||||
{isShowEdit && (
|
||||
<ConfigParamModal
|
||||
appId={appId}
|
||||
isShow
|
||||
isInit={!annotationConfig?.enabled}
|
||||
onHide={() => {
|
||||
setIsShowEdit(false)
|
||||
}}
|
||||
onSave={async (embeddingModel, score) => {
|
||||
if (
|
||||
embeddingModel.embedding_model_name !== annotationConfig?.embedding_model?.embedding_model_name
|
||||
&& embeddingModel.embedding_provider_name !== annotationConfig?.embedding_model?.embedding_provider_name
|
||||
) {
|
||||
const { job_id: jobId }: any = await updateAnnotationStatus(appId, AnnotationEnableStatus.enable, embeddingModel, score)
|
||||
await ensureJobCompleted(jobId, AnnotationEnableStatus.enable)
|
||||
}
|
||||
|
||||
if (score !== annotationConfig?.score_threshold)
|
||||
await updateAnnotationScore(appId, annotationConfig?.id || '', score)
|
||||
|
||||
await fetchAnnotationConfig()
|
||||
Toast.notify({
|
||||
message: t('common.api.actionSuccess'),
|
||||
type: 'success',
|
||||
})
|
||||
setIsShowEdit(false)
|
||||
}}
|
||||
annotationConfig={annotationConfig!}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
isShowAnnotationFullModal && (
|
||||
<AnnotationFullModal
|
||||
show={isShowAnnotationFullModal}
|
||||
onHide={() => setIsShowAnnotationFullModal(false)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Annotation)
|
98
web/app/components/app/annotation/list.tsx
Normal file
|
@ -0,0 +1,98 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import dayjs from 'dayjs'
|
||||
import { Edit02, Trash03 } from '../../base/icons/src/vender/line/general'
|
||||
import s from './style.module.css'
|
||||
import type { AnnotationItem } from './type'
|
||||
import RemoveAnnotationConfirmModal from './remove-annotation-confirm-modal'
|
||||
|
||||
type Props = {
|
||||
list: AnnotationItem[]
|
||||
onRemove: (id: string) => void
|
||||
onView: (item: AnnotationItem) => void
|
||||
}
|
||||
|
||||
const List: FC<Props> = ({
|
||||
list,
|
||||
onView,
|
||||
onRemove,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [currId, setCurrId] = React.useState<string | null>(null)
|
||||
const [showConfirmDelete, setShowConfirmDelete] = React.useState(false)
|
||||
return (
|
||||
<div className='overflow-x-auto'>
|
||||
<table className={cn(s.logTable, 'w-full min-w-[440px] border-collapse border-0 text-sm')} >
|
||||
<thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-bold">
|
||||
<tr className='uppercase'>
|
||||
<td className='whitespace-nowrap'>{t('appAnnotation.table.header.question')}</td>
|
||||
<td className='whitespace-nowrap'>{t('appAnnotation.table.header.answer')}</td>
|
||||
<td className='whitespace-nowrap'>{t('appAnnotation.table.header.createdAt')}</td>
|
||||
<td className='whitespace-nowrap'>{t('appAnnotation.table.header.hits')}</td>
|
||||
<td className='whitespace-nowrap w-[96px]'>{t('appAnnotation.table.header.actions')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-gray-500">
|
||||
{list.map(item => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className={'border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer'}
|
||||
onClick={
|
||||
() => {
|
||||
onView(item)
|
||||
}
|
||||
}
|
||||
>
|
||||
<td
|
||||
className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
|
||||
title={item.question}
|
||||
>{item.question}</td>
|
||||
<td
|
||||
className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
|
||||
title={item.answer}
|
||||
>{item.answer}</td>
|
||||
<td>{dayjs(item.created_at * 1000).format('YYYY-MM-DD hh:mm')}</td>
|
||||
<td>{item.hit_count}</td>
|
||||
<td className='w-[96px]' onClick={e => e.stopPropagation()}>
|
||||
{/* Actions */}
|
||||
<div className='flex space-x-2 text-gray-500'>
|
||||
<div
|
||||
className='p-1 cursor-pointer rounded-md hover:bg-black/5'
|
||||
onClick={
|
||||
() => {
|
||||
onView(item)
|
||||
}
|
||||
}
|
||||
>
|
||||
<Edit02 className='w-4 h-4' />
|
||||
</div>
|
||||
<div
|
||||
className='p-1 cursor-pointer rounded-md hover:bg-black/5'
|
||||
onClick={() => {
|
||||
setCurrId(item.id)
|
||||
setShowConfirmDelete(true)
|
||||
}}
|
||||
>
|
||||
<Trash03 className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<RemoveAnnotationConfirmModal
|
||||
isShow={showConfirmDelete}
|
||||
onHide={() => setShowConfirmDelete(false)}
|
||||
onRemove={() => {
|
||||
onRemove(currId as string)
|
||||
setShowConfirmDelete(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(List)
|
|
@ -0,0 +1,29 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import DeleteConfirmModal from '@/app/components/base/modal/delete-confirm-modal'
|
||||
|
||||
type Props = {
|
||||
isShow: boolean
|
||||
onHide: () => void
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
const RemoveAnnotationConfirmModal: FC<Props> = ({
|
||||
isShow,
|
||||
onHide,
|
||||
onRemove,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<DeleteConfirmModal
|
||||
isShow={isShow}
|
||||
onHide={onHide}
|
||||
onRemove={onRemove}
|
||||
text={t('appDebug.feature.annotation.removeConfirm') as string}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(RemoveAnnotationConfirmModal)
|
9
web/app/components/app/annotation/style.module.css
Normal file
|
@ -0,0 +1,9 @@
|
|||
.logTable td {
|
||||
padding: 7px 8px;
|
||||
box-sizing: border-box;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.pagination li {
|
||||
list-style: none;
|
||||
}
|
39
web/app/components/app/annotation/type.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
export type AnnotationItemBasic = {
|
||||
message_id?: string
|
||||
question: string
|
||||
answer: string
|
||||
}
|
||||
|
||||
export type AnnotationItem = {
|
||||
id: string
|
||||
question: string
|
||||
answer: string
|
||||
created_at: number
|
||||
hit_count: number
|
||||
}
|
||||
|
||||
export type HitHistoryItem = {
|
||||
id: string
|
||||
question: string
|
||||
match: string
|
||||
response: string
|
||||
source: string
|
||||
score: number
|
||||
created_at: number
|
||||
}
|
||||
|
||||
export type EmbeddingModelConfig = {
|
||||
embedding_provider_name: string
|
||||
embedding_model_name: string
|
||||
}
|
||||
|
||||
export enum AnnotationEnableStatus {
|
||||
enable = 'enable',
|
||||
disable = 'disable',
|
||||
}
|
||||
|
||||
export enum JobStatus {
|
||||
waiting = 'waiting',
|
||||
processing = 'processing',
|
||||
completed = 'completed',
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ClockFastForward } from '@/app/components/base/icons/src/vender/line/time'
|
||||
|
||||
const HitHistoryNoData: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='mx-auto mt-20 w-[480px] p-5 rounded-2xl bg-gray-50 space-y-2'>
|
||||
<div className='inline-block p-3 rounded-lg border border-gray-200'>
|
||||
<ClockFastForward className='w-5 h-5 text-gray-500' />
|
||||
</div>
|
||||
<div className='leading-5 text-sm font-normal text-gray-500'>{t('appAnnotation.viewModal.noHitHistory')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(HitHistoryNoData)
|
|
@ -0,0 +1,237 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import dayjs from 'dayjs'
|
||||
import { Pagination } from 'react-headless-pagination'
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline'
|
||||
import EditItem, { EditItemType } from '../edit-annotation-modal/edit-item'
|
||||
import type { AnnotationItem, HitHistoryItem } from '../type'
|
||||
import s from './style.module.css'
|
||||
import HitHistoryNoData from './hit-history-no-data'
|
||||
import Drawer from '@/app/components/base/drawer-plus'
|
||||
import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication'
|
||||
import DeleteConfirmModal from '@/app/components/base/modal/delete-confirm-modal'
|
||||
import TabSlider from '@/app/components/base/tab-slider-plain'
|
||||
import { fetchHitHistoryList } from '@/service/annotation'
|
||||
import { APP_PAGE_LIMIT } from '@/config'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
isShow: boolean
|
||||
onHide: () => void
|
||||
item: AnnotationItem
|
||||
onSave: (editedQuery: string, editedAnswer: string) => void
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
enum TabType {
|
||||
annotation = 'annotation',
|
||||
hitHistory = 'hitHistory',
|
||||
}
|
||||
|
||||
const ViewAnnotationModal: FC<Props> = ({
|
||||
appId,
|
||||
isShow,
|
||||
onHide,
|
||||
item,
|
||||
onSave,
|
||||
onRemove,
|
||||
}) => {
|
||||
const { id, question, answer, created_at: createdAt } = item
|
||||
const [newQuestion, setNewQuery] = useState(question)
|
||||
const [newAnswer, setNewAnswer] = useState(answer)
|
||||
const { t } = useTranslation()
|
||||
const [currPage, setCurrPage] = React.useState<number>(0)
|
||||
const [total, setTotal] = useState(0)
|
||||
const [hitHistoryList, setHitHistoryList] = useState<HitHistoryItem[]>([])
|
||||
const fetchHitHistory = async (page = 1) => {
|
||||
try {
|
||||
const { data, total }: any = await fetchHitHistoryList(appId, id, {
|
||||
page,
|
||||
limit: 10,
|
||||
})
|
||||
setHitHistoryList(data as HitHistoryItem[])
|
||||
setTotal(total)
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchHitHistory(currPage + 1)
|
||||
}, [currPage])
|
||||
|
||||
const tabs = [
|
||||
{ value: TabType.annotation, text: t('appAnnotation.viewModal.annotatedResponse') },
|
||||
{
|
||||
value: TabType.hitHistory,
|
||||
text: (
|
||||
hitHistoryList.length > 0
|
||||
? (
|
||||
<div className='flex items-center space-x-1'>
|
||||
<div>{t('appAnnotation.viewModal.hitHistory')}</div>
|
||||
<div className='flex px-1.5 item-center rounded-md border border-black/[8%] h-5 text-xs font-medium text-gray-500'>{total} {t(`appAnnotation.viewModal.hit${hitHistoryList.length > 1 ? 's' : ''}`)}</div>
|
||||
</div>
|
||||
)
|
||||
: t('appAnnotation.viewModal.hitHistory')
|
||||
),
|
||||
},
|
||||
]
|
||||
const [activeTab, setActiveTab] = useState(TabType.annotation)
|
||||
const handleSave = (type: EditItemType, editedContent: string) => {
|
||||
if (type === EditItemType.Query) {
|
||||
setNewQuery(editedContent)
|
||||
onSave(editedContent, newAnswer)
|
||||
}
|
||||
else {
|
||||
setNewAnswer(editedContent)
|
||||
onSave(newQuestion, editedContent)
|
||||
}
|
||||
}
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
|
||||
const annotationTab = (
|
||||
<>
|
||||
<EditItem
|
||||
type={EditItemType.Query}
|
||||
content={question}
|
||||
onSave={editedContent => handleSave(EditItemType.Query, editedContent)}
|
||||
/>
|
||||
<EditItem
|
||||
type={EditItemType.Answer}
|
||||
content={answer}
|
||||
onSave={editedContent => handleSave(EditItemType.Answer, editedContent)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
const hitHistoryTab = total === 0
|
||||
? (<HitHistoryNoData />)
|
||||
: (
|
||||
<div>
|
||||
<table className={cn(s.table, 'w-full min-w-[440px] border-collapse border-0 text-sm')} >
|
||||
<thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-bold">
|
||||
<tr className='uppercase'>
|
||||
<td className='whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.query')}</td>
|
||||
<td className='whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.match')}</td>
|
||||
<td className='whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.response')}</td>
|
||||
<td className='whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.source')}</td>
|
||||
<td className='whitespace-nowrap'>{t('appAnnotation.hitHistoryTable.score')}</td>
|
||||
<td className='whitespace-nowrap w-[140px]'>{t('appAnnotation.hitHistoryTable.time')}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-gray-500">
|
||||
{hitHistoryList.map(item => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className={'border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer'}
|
||||
>
|
||||
<td
|
||||
className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
|
||||
title={item.question}
|
||||
>{item.question}</td>
|
||||
<td
|
||||
className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
|
||||
title={item.match}
|
||||
>{item.match}</td>
|
||||
<td
|
||||
className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
|
||||
title={item.response}
|
||||
>{item.response}</td>
|
||||
<td>{item.source}</td>
|
||||
<td>{item.score ? item.score.toFixed(2) : '-'}</td>
|
||||
<td>{dayjs(item.created_at * 1000).format('YYYY-MM-DD hh:mm')}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{(total && total > APP_PAGE_LIMIT)
|
||||
? <Pagination
|
||||
className="flex items-center w-full h-10 text-sm select-none mt-8"
|
||||
currentPage={currPage}
|
||||
edgePageCount={2}
|
||||
middlePagesSiblingCount={1}
|
||||
setCurrentPage={setCurrPage}
|
||||
totalPages={Math.ceil(total / APP_PAGE_LIMIT)}
|
||||
truncableClassName="w-8 px-0.5 text-center"
|
||||
truncableText="..."
|
||||
>
|
||||
<Pagination.PrevButton
|
||||
disabled={currPage === 0}
|
||||
className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === 0 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} >
|
||||
<ArrowLeftIcon className="mr-3 h-3 w-3" />
|
||||
{t('appLog.table.pagination.previous')}
|
||||
</Pagination.PrevButton>
|
||||
<div className={`flex items-center justify-center flex-grow ${s.pagination}`}>
|
||||
<Pagination.PageButton
|
||||
activeClassName="bg-primary-50 dark:bg-opacity-0 text-primary-600 dark:text-white"
|
||||
className="flex items-center justify-center h-8 w-8 rounded-full cursor-pointer"
|
||||
inactiveClassName="text-gray-500"
|
||||
/>
|
||||
</div>
|
||||
<Pagination.NextButton
|
||||
disabled={currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1}
|
||||
className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} >
|
||||
{t('appLog.table.pagination.next')}
|
||||
<ArrowRightIcon className="ml-3 h-3 w-3" />
|
||||
</Pagination.NextButton>
|
||||
</Pagination>
|
||||
: null}
|
||||
</div>
|
||||
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
<Drawer
|
||||
isShow={isShow}
|
||||
onHide={onHide}
|
||||
maxWidthClassName='!max-w-[800px]'
|
||||
// t('appAnnotation.editModal.title') as string
|
||||
title={
|
||||
<TabSlider
|
||||
className='shrink-0 relative top-[9px]'
|
||||
value={activeTab}
|
||||
onChange={v => setActiveTab(v as TabType)}
|
||||
options={tabs}
|
||||
noBorderBottom
|
||||
itemClassName='!pb-3.5'
|
||||
/>
|
||||
}
|
||||
body={(
|
||||
<div className='p-6 pb-4 space-y-6'>
|
||||
{activeTab === TabType.annotation ? annotationTab : hitHistoryTab}
|
||||
</div>
|
||||
)}
|
||||
foot={id
|
||||
? (
|
||||
<div className='px-4 flex h-16 items-center justify-between border-t border-black/5 bg-gray-50 rounded-bl-xl rounded-br-xl leading-[18px] text-[13px] font-medium text-gray-500'>
|
||||
<div
|
||||
className='flex items-center pl-3 space-x-2 cursor-pointer'
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
<MessageCheckRemove />
|
||||
<div>{t('appAnnotation.editModal.removeThisCache')}</div>
|
||||
</div>
|
||||
<div>{t('appAnnotation.editModal.createdAt')} {dayjs(createdAt * 1000).format('YYYY-MM-DD hh:mm')}</div>
|
||||
</div>
|
||||
)
|
||||
: undefined}
|
||||
>
|
||||
</Drawer>
|
||||
<DeleteConfirmModal
|
||||
isShow={showModal}
|
||||
onHide={() => setShowModal(false)}
|
||||
onRemove={async () => {
|
||||
await onRemove()
|
||||
setShowModal(false)
|
||||
onHide()
|
||||
}}
|
||||
text={t('appDebug.feature.annotation.removeConfirm') as string}
|
||||
/>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
export default React.memo(ViewAnnotationModal)
|
|
@ -0,0 +1,9 @@
|
|||
.table td {
|
||||
padding: 7px 8px;
|
||||
box-sizing: border-box;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.pagination li {
|
||||
list-style: none;
|
||||
}
|
|
@ -2,26 +2,26 @@
|
|||
import type { FC, ReactNode } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { UserCircleIcon } from '@heroicons/react/24/solid'
|
||||
import cn from 'classnames'
|
||||
import type { CitationItem, DisplayScene, FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc, ThoughtItem } from '../type'
|
||||
import type { CitationItem, DisplayScene, FeedbackFunc, Feedbacktype, IChatItem, ThoughtItem } from '../type'
|
||||
import OperationBtn from '../operation'
|
||||
import LoadingAnim from '../loading-anim'
|
||||
import { EditIcon, EditIconSolid, OpeningStatementIcon, RatingIcon } from '../icon-component'
|
||||
import { EditIconSolid, OpeningStatementIcon, RatingIcon } from '../icon-component'
|
||||
import s from '../style.module.css'
|
||||
import MoreInfo from '../more-info'
|
||||
import CopyBtn from '../copy-btn'
|
||||
import Thought from '../thought'
|
||||
import Citation from '../citation'
|
||||
import { randomString } from '@/utils'
|
||||
import type { Annotation, MessageRating } from '@/models/log'
|
||||
import AppContext from '@/context/app-context'
|
||||
import type { MessageRating } from '@/models/log'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import AutoHeightTextarea from '@/app/components/base/auto-height-textarea'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import AnnotationCtrlBtn from '@/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn'
|
||||
import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
|
||||
import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
|
||||
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
|
||||
const Divider: FC<{ name: string }> = ({ name }) => {
|
||||
const { t } = useTranslation()
|
||||
|
@ -42,7 +42,6 @@ export type IAnswerProps = {
|
|||
feedbackDisabled: boolean
|
||||
isHideFeedbackEdit: boolean
|
||||
onFeedback?: FeedbackFunc
|
||||
onSubmitAnnotation?: SubmitAnnotationFunc
|
||||
displayScene: DisplayScene
|
||||
isResponsing?: boolean
|
||||
answerIcon?: ReactNode
|
||||
|
@ -52,6 +51,13 @@ export type IAnswerProps = {
|
|||
dataSets?: DataSet[]
|
||||
isShowCitation?: boolean
|
||||
isShowCitationHitInfo?: boolean
|
||||
// Annotation props
|
||||
supportAnnotation?: boolean
|
||||
appId?: string
|
||||
question: string
|
||||
onAnnotationEdited?: (question: string, answer: string) => void
|
||||
onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string) => void
|
||||
onAnnotationRemoved?: () => void
|
||||
}
|
||||
// The component needs to maintain its own state to control whether to display input component
|
||||
const Answer: FC<IAnswerProps> = ({
|
||||
|
@ -59,7 +65,6 @@ const Answer: FC<IAnswerProps> = ({
|
|||
feedbackDisabled = false,
|
||||
isHideFeedbackEdit = false,
|
||||
onFeedback,
|
||||
onSubmitAnnotation,
|
||||
displayScene = 'web',
|
||||
isResponsing,
|
||||
answerIcon,
|
||||
|
@ -69,15 +74,25 @@ const Answer: FC<IAnswerProps> = ({
|
|||
dataSets,
|
||||
isShowCitation,
|
||||
isShowCitationHitInfo = false,
|
||||
supportAnnotation,
|
||||
appId,
|
||||
question,
|
||||
onAnnotationEdited,
|
||||
onAnnotationAdded,
|
||||
onAnnotationRemoved,
|
||||
}) => {
|
||||
const { id, content, more, feedback, adminFeedback, annotation: initAnnotation } = item
|
||||
const { id, content, more, feedback, adminFeedback, annotation } = item
|
||||
const hasAnnotation = !!annotation?.id
|
||||
const [showEdit, setShowEdit] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [annotation, setAnnotation] = useState<Annotation | undefined | null>(initAnnotation)
|
||||
const [inputValue, setInputValue] = useState<string>(initAnnotation?.content ?? '')
|
||||
// const [annotation, setAnnotation] = useState<Annotation | undefined | null>(initAnnotation)
|
||||
// const [inputValue, setInputValue] = useState<string>(initAnnotation?.content ?? '')
|
||||
const [localAdminFeedback, setLocalAdminFeedback] = useState<Feedbacktype | undefined | null>(adminFeedback)
|
||||
const { userProfile } = useContext(AppContext)
|
||||
// const { userProfile } = useContext(AppContext)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isShowReplyModal, setIsShowReplyModal] = useState(false)
|
||||
|
||||
/**
|
||||
* Render feedback results (distinguish between users and administrators)
|
||||
* User reviews cannot be cancelled in Console
|
||||
|
@ -121,6 +136,19 @@ const Answer: FC<IAnswerProps> = ({
|
|||
)
|
||||
}
|
||||
|
||||
const renderHasAnnotationBtn = () => {
|
||||
return (
|
||||
<div
|
||||
className={cn(s.hasAnnotationBtn, 'relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7]')}
|
||||
style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }}
|
||||
>
|
||||
<div className='p-1 rounded-lg bg-[#EEF4FF] '>
|
||||
<MessageFast className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Different scenarios have different operation items.
|
||||
* @param isWebScene Whether it is web scene
|
||||
|
@ -142,12 +170,6 @@ const Answer: FC<IAnswerProps> = ({
|
|||
|
||||
const adminOperation = () => {
|
||||
return <div className='flex gap-1'>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.addAnnotation') as string}>
|
||||
{OperationBtn({
|
||||
innerContent: <IconWrapper><EditIcon className='hover:text-gray-800' /></IconWrapper>,
|
||||
onClick: () => setShowEdit(true),
|
||||
})}
|
||||
</Tooltip>
|
||||
{!localAdminFeedback?.rating && <>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.like') as string}>
|
||||
{OperationBtn({
|
||||
|
@ -219,47 +241,27 @@ const Answer: FC<IAnswerProps> = ({
|
|||
)
|
||||
: (
|
||||
<div>
|
||||
<Markdown content={content} />
|
||||
{annotation?.logAnnotation && (
|
||||
<div className='mb-1'>
|
||||
<div className='mb-3'>
|
||||
<Markdown className='line-through !text-gray-400' content={content} />
|
||||
</div>
|
||||
<EditTitle title={t('appAnnotation.editBy', {
|
||||
author: annotation?.logAnnotation.account.name,
|
||||
})} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Markdown content={annotation?.logAnnotation ? annotation?.logAnnotation.content : content} />
|
||||
</div>
|
||||
{(hasAnnotation && !annotation?.logAnnotation) && (
|
||||
<EditTitle className='mt-1' title={t('appAnnotation.editBy', {
|
||||
author: annotation.authorName,
|
||||
})} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!showEdit
|
||||
? (annotation?.content
|
||||
&& <>
|
||||
<Divider name={annotation?.account?.name || userProfile?.name} />
|
||||
{annotation.content}
|
||||
</>)
|
||||
: <>
|
||||
<Divider name={annotation?.account?.name || userProfile?.name} />
|
||||
<AutoHeightTextarea
|
||||
placeholder={t('appLog.detail.operation.annotationPlaceholder') as string}
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
minHeight={58}
|
||||
className={`${cn(s.textArea)} !py-2 resize-none block w-full !px-3 bg-gray-50 border border-gray-200 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm text-gray-700 tracking-[0.2px]`}
|
||||
/>
|
||||
<div className="mt-2 flex flex-row">
|
||||
<Button
|
||||
type='primary'
|
||||
className='mr-2'
|
||||
loading={loading}
|
||||
onClick={async () => {
|
||||
if (!inputValue)
|
||||
return
|
||||
setLoading(true)
|
||||
const res = await onSubmitAnnotation?.(id, inputValue)
|
||||
if (res)
|
||||
setAnnotation({ ...annotation, content: inputValue } as Annotation)
|
||||
setLoading(false)
|
||||
setShowEdit(false)
|
||||
}}>{t('common.operation.confirm')}</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setInputValue(annotation?.content ?? '')
|
||||
setShowEdit(false)
|
||||
}}>{t('common.operation.cancel')}</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{
|
||||
!!citation?.length && !isThinking && isShowCitation && !isResponsing && (
|
||||
<Citation data={citation} showHitInfo={isShowCitationHitInfo} />
|
||||
|
@ -273,6 +275,36 @@ const Answer: FC<IAnswerProps> = ({
|
|||
className={cn(s.copyBtn, 'mr-1')}
|
||||
/>
|
||||
)}
|
||||
{supportAnnotation && (
|
||||
<AnnotationCtrlBtn
|
||||
appId={appId!}
|
||||
messageId={id}
|
||||
annotationId={annotation?.id || ''}
|
||||
className={cn(s.annotationBtn, 'ml-1')}
|
||||
cached={hasAnnotation}
|
||||
query={question}
|
||||
answer={content}
|
||||
onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content)}
|
||||
onEdit={() => setIsShowReplyModal(true)}
|
||||
onRemoved={onAnnotationRemoved!}
|
||||
/>
|
||||
)}
|
||||
|
||||
<EditReplyModal
|
||||
isShow={isShowReplyModal}
|
||||
onHide={() => setIsShowReplyModal(false)}
|
||||
query={question}
|
||||
answer={content}
|
||||
onEdited={onAnnotationEdited!}
|
||||
onAdded={onAnnotationAdded!}
|
||||
appId={appId!}
|
||||
messageId={id}
|
||||
annotationId={annotation?.id || ''}
|
||||
createdAt={annotation?.created_at}
|
||||
onRemove={() => { }}
|
||||
/>
|
||||
{hasAnnotation && renderHasAnnotationBtn()}
|
||||
|
||||
{!feedbackDisabled && !item.feedbackDisabled && renderItemOperation(displayScene !== 'console')}
|
||||
{/* Admin feedback is displayed only in the background. */}
|
||||
{!feedbackDisabled && renderFeedbackRating(localAdminFeedback?.rating, false, false)}
|
||||
|
@ -280,6 +312,7 @@ const Answer: FC<IAnswerProps> = ({
|
|||
{!feedbackDisabled && renderFeedbackRating(feedback?.rating, !isHideFeedbackEdit, displayScene !== 'console')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{more && <MoreInfo className='invisible group-hover:visible' more={more} isQuestion={false} />}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,7 +7,7 @@ import cn from 'classnames'
|
|||
import Recorder from 'js-audio-recorder'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import s from './style.module.css'
|
||||
import type { DisplayScene, FeedbackFunc, IChatItem, SubmitAnnotationFunc } from './type'
|
||||
import type { DisplayScene, FeedbackFunc, IChatItem } from './type'
|
||||
import { TryToAskIcon, stopIcon } from './icon-component'
|
||||
import Answer from './answer'
|
||||
import Question from './question'
|
||||
|
@ -24,10 +24,13 @@ import ChatImageUploader from '@/app/components/base/image-uploader/chat-image-u
|
|||
import ImageList from '@/app/components/base/image-uploader/image-list'
|
||||
import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app'
|
||||
import { useClipboardUploader, useDraggableUploader, useImageFiles } from '@/app/components/base/image-uploader/hooks'
|
||||
import type { Annotation } from '@/models/log'
|
||||
|
||||
export type IChatProps = {
|
||||
appId?: string
|
||||
configElem?: React.ReactNode
|
||||
chatList: IChatItem[]
|
||||
onChatListChange?: (chatList: IChatItem[]) => void
|
||||
controlChatUpdateAllConversation?: number
|
||||
/**
|
||||
* Whether to display the editing area and rating status
|
||||
|
@ -39,7 +42,6 @@ export type IChatProps = {
|
|||
isHideFeedbackEdit?: boolean
|
||||
isHideSendInput?: boolean
|
||||
onFeedback?: FeedbackFunc
|
||||
onSubmitAnnotation?: SubmitAnnotationFunc
|
||||
checkCanSend?: () => boolean
|
||||
onSend?: (message: string, files: VisionFile[]) => void
|
||||
displayScene?: DisplayScene
|
||||
|
@ -59,6 +61,7 @@ export type IChatProps = {
|
|||
isShowCitationHitInfo?: boolean
|
||||
isShowPromptLog?: boolean
|
||||
visionConfig?: VisionSettings
|
||||
supportAnnotation?: boolean
|
||||
}
|
||||
|
||||
const Chat: FC<IChatProps> = ({
|
||||
|
@ -69,7 +72,6 @@ const Chat: FC<IChatProps> = ({
|
|||
isHideFeedbackEdit = false,
|
||||
isHideSendInput = false,
|
||||
onFeedback,
|
||||
onSubmitAnnotation,
|
||||
checkCanSend,
|
||||
onSend = () => { },
|
||||
displayScene,
|
||||
|
@ -89,6 +91,9 @@ const Chat: FC<IChatProps> = ({
|
|||
isShowCitationHitInfo,
|
||||
isShowPromptLog,
|
||||
visionConfig,
|
||||
appId,
|
||||
supportAnnotation,
|
||||
onChatListChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
|
@ -190,7 +195,7 @@ const Chat: FC<IChatProps> = ({
|
|||
{isShowConfigElem && (configElem || null)}
|
||||
{/* Chat List */}
|
||||
<div className={cn((isShowConfigElem && configElem) ? 'h-0' : 'h-full', 'space-y-[30px]')}>
|
||||
{chatList.map((item) => {
|
||||
{chatList.map((item, index) => {
|
||||
if (item.isAnswer) {
|
||||
const isLast = item.id === chatList[chatList.length - 1].id
|
||||
const thoughts = item.agent_thoughts?.filter(item => item.thought !== '[DONE]')
|
||||
|
@ -202,7 +207,6 @@ const Chat: FC<IChatProps> = ({
|
|||
feedbackDisabled={feedbackDisabled}
|
||||
isHideFeedbackEdit={isHideFeedbackEdit}
|
||||
onFeedback={onFeedback}
|
||||
onSubmitAnnotation={onSubmitAnnotation}
|
||||
displayScene={displayScene ?? 'web'}
|
||||
isResponsing={isResponsing && isLast}
|
||||
answerIcon={answerIcon}
|
||||
|
@ -212,6 +216,72 @@ const Chat: FC<IChatProps> = ({
|
|||
dataSets={dataSets}
|
||||
isShowCitation={isShowCitation}
|
||||
isShowCitationHitInfo={isShowCitationHitInfo}
|
||||
supportAnnotation={supportAnnotation}
|
||||
appId={appId}
|
||||
question={chatList[index - 1]?.content}
|
||||
onAnnotationEdited={(query, answer) => {
|
||||
onChatListChange?.(chatList.map((item, i) => {
|
||||
if (i === index - 1) {
|
||||
return {
|
||||
...item,
|
||||
content: query,
|
||||
}
|
||||
}
|
||||
if (i === index) {
|
||||
return {
|
||||
...item,
|
||||
content: answer,
|
||||
}
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}}
|
||||
onAnnotationAdded={(annotationId, authorName, query, answer) => {
|
||||
onChatListChange?.(chatList.map((item, i) => {
|
||||
if (i === index - 1) {
|
||||
return {
|
||||
...item,
|
||||
content: query,
|
||||
}
|
||||
}
|
||||
if (i === index) {
|
||||
const answerItem = {
|
||||
...item,
|
||||
content: item.content,
|
||||
annotation: {
|
||||
id: annotationId,
|
||||
authorName,
|
||||
logAnnotation: {
|
||||
content: answer,
|
||||
account: {
|
||||
id: '',
|
||||
name: authorName,
|
||||
email: '',
|
||||
},
|
||||
},
|
||||
} as Annotation,
|
||||
}
|
||||
return answerItem
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}}
|
||||
onAnnotationRemoved={() => {
|
||||
onChatListChange?.(chatList.map((item, i) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
...item,
|
||||
content: item.content,
|
||||
annotation: {
|
||||
...(item.annotation || {}),
|
||||
id: '',
|
||||
} as Annotation,
|
||||
}
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}}
|
||||
|
||||
/>
|
||||
}
|
||||
return (
|
||||
|
|
|
@ -38,7 +38,8 @@
|
|||
background: url(./icons/answer.svg) no-repeat;
|
||||
}
|
||||
|
||||
.copyBtn {
|
||||
.copyBtn,
|
||||
.annotationBtn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -63,10 +64,15 @@
|
|||
max-width: 100%;
|
||||
}
|
||||
|
||||
.answerWrap:hover .copyBtn {
|
||||
.answerWrap:hover .copyBtn,
|
||||
.answerWrap:hover .annotationBtn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.answerWrap:hover .hasAnnotationBtn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.answerWrap .itemOperation {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -81,3 +81,12 @@ export type MessageReplace = {
|
|||
answer: string
|
||||
conversation_id: string
|
||||
}
|
||||
|
||||
export type AnnotationReply = {
|
||||
id: string
|
||||
task_id: string
|
||||
answer: string
|
||||
conversation_id: string
|
||||
annotation_id: string
|
||||
annotation_author_name: string
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import SuggestedQuestionsAfterAnswerIcon from '@/app/components/app/configuratio
|
|||
import { Microphone01 } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
import { Citations } from '@/app/components/base/icons/src/vender/solid/editor'
|
||||
import { FileSearch02 } from '@/app/components/base/icons/src/vender/solid/files'
|
||||
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
type IConfig = {
|
||||
openingStatement: boolean
|
||||
moreLikeThis: boolean
|
||||
|
@ -17,6 +18,7 @@ type IConfig = {
|
|||
speechToText: boolean
|
||||
citation: boolean
|
||||
moderation: boolean
|
||||
annotation: boolean
|
||||
}
|
||||
|
||||
export type IChooseFeatureProps = {
|
||||
|
@ -43,7 +45,6 @@ const ChooseFeature: FC<IChooseFeatureProps> = ({
|
|||
showSpeechToTextItem,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
|
@ -126,10 +127,18 @@ const ChooseFeature: FC<IChooseFeatureProps> = ({
|
|||
value={config.moderation}
|
||||
onChange={value => onChange('moderation', value)}
|
||||
/>
|
||||
{isChatApp && (
|
||||
<FeatureItem
|
||||
icon={<MessageFast className='w-4 h-4 text-[#444CE7]' />}
|
||||
title={t('appDebug.feature.annotation.title')}
|
||||
description={t('appDebug.feature.annotation.description')}
|
||||
value={config.annotation}
|
||||
onChange={value => onChange('annotation', value)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</FeatureGroup>
|
||||
</div>
|
||||
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ function useFeature({
|
|||
setSpeechToText,
|
||||
citation,
|
||||
setCitation,
|
||||
annotation,
|
||||
setAnnotation,
|
||||
moderation,
|
||||
setModeration,
|
||||
}: {
|
||||
|
@ -24,6 +26,8 @@ function useFeature({
|
|||
setSpeechToText: (speechToText: boolean) => void
|
||||
citation: boolean
|
||||
setCitation: (citation: boolean) => void
|
||||
annotation: boolean
|
||||
setAnnotation: (annotation: boolean) => void
|
||||
moderation: boolean
|
||||
setModeration: (moderation: boolean) => void
|
||||
}) {
|
||||
|
@ -45,6 +49,7 @@ function useFeature({
|
|||
suggestedQuestionsAfterAnswer,
|
||||
speechToText,
|
||||
citation,
|
||||
annotation,
|
||||
moderation,
|
||||
}
|
||||
const handleFeatureChange = (key: string, value: boolean) => {
|
||||
|
@ -67,6 +72,9 @@ function useFeature({
|
|||
case 'citation':
|
||||
setCitation(value)
|
||||
break
|
||||
case 'annotation':
|
||||
setAnnotation(value)
|
||||
break
|
||||
case 'moderation':
|
||||
setModeration(value)
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import ExperienceEnchanceGroup from '../features/experience-enchance-group'
|
|||
import Toolbox from '../toolbox'
|
||||
import HistoryPanel from '../config-prompt/conversation-histroy/history-panel'
|
||||
import ConfigVision from '../config-vision'
|
||||
import useAnnotationConfig from '../toolbox/annotation/use-annotation-config'
|
||||
import AddFeatureBtn from './feature/add-feature-btn'
|
||||
import ChooseFeature from './feature/choose-feature'
|
||||
import useFeature from './feature/use-feature'
|
||||
|
@ -18,13 +19,16 @@ import AdvancedModeWaring from '@/app/components/app/configuration/prompt-mode/a
|
|||
import ConfigContext from '@/context/debug-configuration'
|
||||
import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
|
||||
import ConfigVar from '@/app/components/app/configuration/config-var'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import type { CitationConfig, ModelConfig, ModerationConfig, MoreLikeThisConfig, PromptVariable, SpeechToTextConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug'
|
||||
import { AppType, ModelModeType } from '@/types/app'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import ConfigParamModal from '@/app/components/app/configuration/toolbox/annotation/config-param-modal'
|
||||
import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
|
||||
|
||||
const Config: FC = () => {
|
||||
const {
|
||||
appId,
|
||||
mode,
|
||||
isAdvancedMode,
|
||||
modelModeType,
|
||||
|
@ -45,6 +49,8 @@ const Config: FC = () => {
|
|||
setSpeechToTextConfig,
|
||||
citationConfig,
|
||||
setCitationConfig,
|
||||
annotationConfig,
|
||||
setAnnotationConfig,
|
||||
moderationConfig,
|
||||
setModerationConfig,
|
||||
} = useContext(ConfigContext)
|
||||
|
@ -56,7 +62,7 @@ const Config: FC = () => {
|
|||
const promptVariables = modelConfig.configs.prompt_variables
|
||||
// simple mode
|
||||
const handlePromptChange = (newTemplate: string, newVariables: PromptVariable[]) => {
|
||||
const newModelConfig = produce(modelConfig, (draft) => {
|
||||
const newModelConfig = produce(modelConfig, (draft: ModelConfig) => {
|
||||
draft.configs.prompt_template = newTemplate
|
||||
draft.configs.prompt_variables = [...draft.configs.prompt_variables, ...newVariables]
|
||||
})
|
||||
|
@ -70,7 +76,7 @@ const Config: FC = () => {
|
|||
|
||||
const handlePromptVariablesNameChange = (newVariables: PromptVariable[]) => {
|
||||
setPrevPromptConfig(modelConfig.configs)
|
||||
const newModelConfig = produce(modelConfig, (draft) => {
|
||||
const newModelConfig = produce(modelConfig, (draft: ModelConfig) => {
|
||||
draft.configs.prompt_variables = newVariables
|
||||
})
|
||||
setModelConfig(newModelConfig)
|
||||
|
@ -85,31 +91,42 @@ const Config: FC = () => {
|
|||
setIntroduction,
|
||||
moreLikeThis: moreLikeThisConfig.enabled,
|
||||
setMoreLikeThis: (value) => {
|
||||
setMoreLikeThisConfig(produce(moreLikeThisConfig, (draft) => {
|
||||
setMoreLikeThisConfig(produce(moreLikeThisConfig, (draft: MoreLikeThisConfig) => {
|
||||
draft.enabled = value
|
||||
}))
|
||||
},
|
||||
suggestedQuestionsAfterAnswer: suggestedQuestionsAfterAnswerConfig.enabled,
|
||||
setSuggestedQuestionsAfterAnswer: (value) => {
|
||||
setSuggestedQuestionsAfterAnswerConfig(produce(suggestedQuestionsAfterAnswerConfig, (draft) => {
|
||||
setSuggestedQuestionsAfterAnswerConfig(produce(suggestedQuestionsAfterAnswerConfig, (draft: SuggestedQuestionsAfterAnswerConfig) => {
|
||||
draft.enabled = value
|
||||
}))
|
||||
},
|
||||
speechToText: speechToTextConfig.enabled,
|
||||
setSpeechToText: (value) => {
|
||||
setSpeechToTextConfig(produce(speechToTextConfig, (draft) => {
|
||||
setSpeechToTextConfig(produce(speechToTextConfig, (draft: SpeechToTextConfig) => {
|
||||
draft.enabled = value
|
||||
}))
|
||||
},
|
||||
citation: citationConfig.enabled,
|
||||
setCitation: (value) => {
|
||||
setCitationConfig(produce(citationConfig, (draft) => {
|
||||
setCitationConfig(produce(citationConfig, (draft: CitationConfig) => {
|
||||
draft.enabled = value
|
||||
}))
|
||||
},
|
||||
annotation: annotationConfig.enabled,
|
||||
setAnnotation: async (value) => {
|
||||
if (value) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
setIsShowAnnotationConfigInit(true)
|
||||
}
|
||||
else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
await handleDisableAnnotation(annotationConfig.embedding_model)
|
||||
}
|
||||
},
|
||||
moderation: moderationConfig.enabled,
|
||||
setModeration: (value) => {
|
||||
setModerationConfig(produce(moderationConfig, (draft) => {
|
||||
setModerationConfig(produce(moderationConfig, (draft: ModerationConfig) => {
|
||||
draft.enabled = value
|
||||
}))
|
||||
if (value && !moderationConfig.type) {
|
||||
|
@ -127,7 +144,7 @@ const Config: FC = () => {
|
|||
},
|
||||
onSaveCallback: setModerationConfig,
|
||||
onCancelCallback: () => {
|
||||
setModerationConfig(produce(moderationConfig, (draft) => {
|
||||
setModerationConfig(produce(moderationConfig, (draft: ModerationConfig) => {
|
||||
draft.enabled = false
|
||||
showChooseFeatureTrue()
|
||||
}))
|
||||
|
@ -138,8 +155,22 @@ const Config: FC = () => {
|
|||
},
|
||||
})
|
||||
|
||||
const {
|
||||
handleEnableAnnotation,
|
||||
setScore,
|
||||
handleDisableAnnotation,
|
||||
isShowAnnotationConfigInit,
|
||||
setIsShowAnnotationConfigInit,
|
||||
isShowAnnotationFullModal,
|
||||
setIsShowAnnotationFullModal,
|
||||
} = useAnnotationConfig({
|
||||
appId,
|
||||
annotationConfig,
|
||||
setAnnotationConfig,
|
||||
})
|
||||
|
||||
const hasChatConfig = isChatApp && (featureConfig.openingStatement || featureConfig.suggestedQuestionsAfterAnswer || (featureConfig.speechToText && !!speech2textDefaultModel) || featureConfig.citation)
|
||||
const hasToolbox = false
|
||||
const hasToolbox = moderationConfig.enabled || featureConfig.annotation
|
||||
|
||||
const wrapRef = useRef<HTMLDivElement>(null)
|
||||
const wrapScroll = useScroll(wrapRef)
|
||||
|
@ -229,10 +260,36 @@ const Config: FC = () => {
|
|||
|
||||
{/* Toolbox */}
|
||||
{
|
||||
moderationConfig.enabled && (
|
||||
<Toolbox showModerationSettings />
|
||||
hasToolbox && (
|
||||
<Toolbox
|
||||
showModerationSettings={moderationConfig.enabled}
|
||||
showAnnotation={isChatApp && featureConfig.annotation}
|
||||
onEmbeddingChange={handleEnableAnnotation}
|
||||
onScoreChange={setScore}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<ConfigParamModal
|
||||
appId={appId}
|
||||
isInit
|
||||
isShow={isShowAnnotationConfigInit}
|
||||
onHide={() => {
|
||||
setIsShowAnnotationConfigInit(false)
|
||||
showChooseFeatureTrue()
|
||||
}}
|
||||
onSave={async (embeddingModel, score) => {
|
||||
await handleEnableAnnotation(embeddingModel, score)
|
||||
setIsShowAnnotationConfigInit(false)
|
||||
}}
|
||||
annotationConfig={annotationConfig}
|
||||
/>
|
||||
{isShowAnnotationFullModal && (
|
||||
<AnnotationFullModal
|
||||
show={isShowAnnotationFullModal}
|
||||
onHide={() => setIsShowAnnotationFullModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -15,9 +15,12 @@ import IconTypeIcon from '@/app/components/app/configuration/config-var/input-ty
|
|||
|
||||
type Option = { name: string; value: string; type: string }
|
||||
export type Props = {
|
||||
triggerClassName?: string
|
||||
className?: string
|
||||
value: string | undefined
|
||||
options: Option[]
|
||||
onChange: (value: string) => void
|
||||
notSelectedVarTip?: string | null
|
||||
}
|
||||
|
||||
const VarItem: FC<{ item: Option }> = ({ item }) => (
|
||||
|
@ -31,9 +34,12 @@ const VarItem: FC<{ item: Option }> = ({ item }) => (
|
|||
</div>
|
||||
)
|
||||
const VarPicker: FC<Props> = ({
|
||||
triggerClassName,
|
||||
className,
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
notSelectedVarTip,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
|
@ -48,9 +54,10 @@ const VarPicker: FC<Props> = ({
|
|||
mainAxis: 8,
|
||||
}}
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
|
||||
<PortalToFollowElemTrigger className={cn(triggerClassName)} onClick={() => setOpen(v => !v)}>
|
||||
<div className={cn(
|
||||
s.trigger,
|
||||
className,
|
||||
notSetVar ? 'bg-[#FFFCF5] border-[#FEDF89] text-[#DC6803]' : ' hover:bg-gray-50 border-gray-200 text-primary-600',
|
||||
open ? 'bg-gray-50' : 'bg-white',
|
||||
`
|
||||
|
@ -63,7 +70,7 @@ const VarPicker: FC<Props> = ({
|
|||
<VarItem item={currItem as Option} />
|
||||
)
|
||||
: (<div>
|
||||
{t('appDebug.feature.dataSet.queryVariable.choosePlaceholder')}
|
||||
{notSelectedVarTip || t('appDebug.feature.dataSet.queryVariable.choosePlaceholder')}
|
||||
</div>)}
|
||||
</div>
|
||||
<ChevronDownIcon className={cn(s.dropdownIcon, open && 'rotate-180 text-[#98A2B3]', 'w-3.5 h-3.5')} />
|
||||
|
|
|
@ -27,7 +27,7 @@ import { IS_CE_EDITION } from '@/config'
|
|||
import { useProviderContext } from '@/context/provider-context'
|
||||
import type { Inputs } from '@/models/debug'
|
||||
import { fetchFileUploadConfig } from '@/service/common'
|
||||
|
||||
import type { Annotation as AnnotationType } from '@/models/log'
|
||||
type IDebug = {
|
||||
hasSetAPIKEY: boolean
|
||||
onSetting: () => void
|
||||
|
@ -67,6 +67,7 @@ const Debug: FC<IDebug> = ({
|
|||
datasetConfigs,
|
||||
externalDataToolsConfig,
|
||||
visionConfig,
|
||||
annotationConfig,
|
||||
} = useContext(ConfigContext)
|
||||
const { speech2textDefaultModel } = useProviderContext()
|
||||
const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
|
||||
|
@ -225,6 +226,7 @@ const Debug: FC<IDebug> = ({
|
|||
file_upload: {
|
||||
image: visionConfig,
|
||||
},
|
||||
annotation_reply: annotationConfig,
|
||||
}
|
||||
|
||||
if (isAdvancedMode) {
|
||||
|
@ -359,6 +361,26 @@ const Debug: FC<IDebug> = ({
|
|||
onMessageReplace: (messageReplace) => {
|
||||
responseItem.content = messageReplace.answer
|
||||
},
|
||||
onAnnotationReply: (annotationReply) => {
|
||||
responseItem.id = annotationReply.id
|
||||
responseItem.content = annotationReply.answer
|
||||
responseItem.annotation = ({
|
||||
id: annotationReply.annotation_id,
|
||||
authorName: annotationReply.annotation_author_name,
|
||||
} as AnnotationType)
|
||||
const newListWithAnswer = produce(
|
||||
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
|
||||
(draft) => {
|
||||
if (!draft.find(item => item.id === questionId))
|
||||
draft.push({ ...questionItem })
|
||||
|
||||
draft.push({
|
||||
...responseItem,
|
||||
id: annotationReply.id,
|
||||
})
|
||||
})
|
||||
setChatList(newListWithAnswer)
|
||||
},
|
||||
onError() {
|
||||
setResponsingFalse()
|
||||
// role back placeholder answer
|
||||
|
@ -477,6 +499,13 @@ const Debug: FC<IDebug> = ({
|
|||
})
|
||||
}
|
||||
|
||||
const varList = modelConfig.configs.prompt_variables.map((item: any) => {
|
||||
return {
|
||||
label: item.key,
|
||||
value: inputs[item.key],
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="shrink-0">
|
||||
|
@ -531,6 +560,9 @@ const Debug: FC<IDebug> = ({
|
|||
...visionConfig,
|
||||
image_file_size_limit: fileUploadConfigResponse?.image_file_size_limit,
|
||||
}}
|
||||
supportAnnotation
|
||||
appId={appId}
|
||||
onChatListChange={setChatList}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -550,6 +582,9 @@ const Debug: FC<IDebug> = ({
|
|||
messageId={messageId}
|
||||
isError={false}
|
||||
onRetry={() => { }}
|
||||
supportAnnotation
|
||||
appId={appId}
|
||||
varList={varList}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -566,7 +601,6 @@ const Debug: FC<IDebug> = ({
|
|||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!hasSetAPIKEY && (<HasNotSetAPIKEY isTrailFinished={!IS_CE_EDITION} onSetting={onSetting} />)}
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -8,7 +8,6 @@ import OpeningStatement from './opening-statement'
|
|||
import SuggestedQuestionsAfterAnswer from './suggested-questions-after-answer'
|
||||
import SpeechToText from './speech-to-text'
|
||||
import Citation from './citation'
|
||||
|
||||
/*
|
||||
* Include
|
||||
* 1. Conversation Opener
|
||||
|
|
|
@ -15,6 +15,7 @@ import s from './style.module.css'
|
|||
import useAdvancedPromptConfig from './hooks/use-advanced-prompt-config'
|
||||
import EditHistoryModal from './config-prompt/conversation-histroy/edit-modal'
|
||||
import type {
|
||||
AnnotationReplyConfig,
|
||||
CompletionParams,
|
||||
DatasetConfigs,
|
||||
Inputs,
|
||||
|
@ -41,7 +42,7 @@ import { useProviderContext } from '@/context/provider-context'
|
|||
import { AppType, ModelModeType, RETRIEVE_TYPE, Resolution, TransferMethod } from '@/types/app'
|
||||
import { FlipBackward } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { PromptMode } from '@/models/debug'
|
||||
import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
|
||||
import { ANNOTATION_DEFAULT, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
|
||||
import SelectDataSet from '@/app/components/app/configuration/dataset-config/select-dataset'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
|
@ -56,6 +57,7 @@ type PublichConfig = {
|
|||
const Configuration: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [formattingChanged, setFormattingChanged] = useState(false)
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
const [hasFetchedDetail, setHasFetchedDetail] = useState(false)
|
||||
const isLoading = !hasFetchedDetail
|
||||
|
@ -89,11 +91,25 @@ const Configuration: FC = () => {
|
|||
const [citationConfig, setCitationConfig] = useState<MoreLikeThisConfig>({
|
||||
enabled: false,
|
||||
})
|
||||
const [annotationConfig, doSetAnnotationConfig] = useState<AnnotationReplyConfig>({
|
||||
id: '',
|
||||
enabled: false,
|
||||
score_threshold: ANNOTATION_DEFAULT.score_threshold,
|
||||
embedding_model: {
|
||||
embedding_provider_name: '',
|
||||
embedding_model_name: '',
|
||||
},
|
||||
})
|
||||
const setAnnotationConfig = (config: AnnotationReplyConfig, notSetFormatChanged?: boolean) => {
|
||||
doSetAnnotationConfig(config)
|
||||
if (!notSetFormatChanged)
|
||||
setFormattingChanged(true)
|
||||
}
|
||||
|
||||
const [moderationConfig, setModerationConfig] = useState<ModerationConfig>({
|
||||
enabled: false,
|
||||
})
|
||||
const [externalDataToolsConfig, setExternalDataToolsConfig] = useState<ExternalDataTool[]>([])
|
||||
const [formattingChanged, setFormattingChanged] = useState(false)
|
||||
const [inputs, setInputs] = useState<Inputs>({})
|
||||
const [query, setQuery] = useState('')
|
||||
const [completionParams, doSetCompletionParams] = useState<CompletionParams>({
|
||||
|
@ -167,7 +183,7 @@ const Configuration: FC = () => {
|
|||
|
||||
setFormattingChanged(true)
|
||||
if (data.find(item => !item.name)) { // has not loaded selected dataset
|
||||
const newSelected = produce(data, (draft) => {
|
||||
const newSelected = produce(data, (draft: any) => {
|
||||
data.forEach((item, index) => {
|
||||
if (!item.name) { // not fetched database
|
||||
const newItem = dataSets.find(i => i.id === item.id)
|
||||
|
@ -230,7 +246,7 @@ const Configuration: FC = () => {
|
|||
if (hasFetchedDetail && !modelModeType) {
|
||||
const mode = textGenerationModelList.find(({ model_name }) => model_name === modelConfig.model_id)?.model_mode
|
||||
if (mode) {
|
||||
const newModelConfig = produce(modelConfig, (draft) => {
|
||||
const newModelConfig = produce(modelConfig, (draft: ModelConfig) => {
|
||||
draft.mode = mode
|
||||
})
|
||||
setModelConfig(newModelConfig)
|
||||
|
@ -302,7 +318,7 @@ const Configuration: FC = () => {
|
|||
await migrateToDefaultPrompt(true, ModelModeType.chat)
|
||||
}
|
||||
}
|
||||
const newModelConfig = produce(modelConfig, (draft) => {
|
||||
const newModelConfig = produce(modelConfig, (draft: ModelConfig) => {
|
||||
draft.provider = provider
|
||||
draft.model_id = modelId
|
||||
draft.mode = modeMode
|
||||
|
@ -369,6 +385,9 @@ const Configuration: FC = () => {
|
|||
if (modelConfig.retriever_resource)
|
||||
setCitationConfig(modelConfig.retriever_resource)
|
||||
|
||||
if (modelConfig.annotation_reply)
|
||||
setAnnotationConfig(modelConfig.annotation_reply, true)
|
||||
|
||||
if (modelConfig.sensitive_word_avoidance)
|
||||
setModerationConfig(modelConfig.sensitive_word_avoidance)
|
||||
|
||||
|
@ -580,6 +599,8 @@ const Configuration: FC = () => {
|
|||
setSpeechToTextConfig,
|
||||
citationConfig,
|
||||
setCitationConfig,
|
||||
annotationConfig,
|
||||
setAnnotationConfig,
|
||||
moderationConfig,
|
||||
setModerationConfig,
|
||||
externalDataToolsConfig,
|
||||
|
@ -628,7 +649,7 @@ const Configuration: FC = () => {
|
|||
onClick={() => setPromptMode(PromptMode.simple)}
|
||||
className='flex items-center h-6 px-2 bg-indigo-600 shadow-xs border border-gray-200 rounded-lg text-white text-xs font-semibold cursor-pointer space-x-1'
|
||||
>
|
||||
<FlipBackward className='w-3 h-3 text-white'/>
|
||||
<FlipBackward className='w-3 h-3 text-white' />
|
||||
<div className='text-xs font-semibold uppercase'>{t('appDebug.promptMode.switchBack')}</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useRef, useState } from 'react'
|
||||
import { useHover } from 'ahooks'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { MessageCheckRemove, MessageFastPlus } from '@/app/components/base/icons/src/vender/line/communication'
|
||||
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import { Edit04 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import RemoveAnnotationConfirmModal from '@/app/components/app/annotation/remove-annotation-confirm-modal'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import { addAnnotation, delAnnotation } from '@/service/annotation'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
messageId?: string
|
||||
annotationId?: string
|
||||
className?: string
|
||||
cached: boolean
|
||||
query: string
|
||||
answer: string
|
||||
onAdded: (annotationId: string, authorName: string) => void
|
||||
onEdit: () => void
|
||||
onRemoved: () => void
|
||||
}
|
||||
|
||||
const CacheCtrlBtn: FC<Props> = ({
|
||||
className,
|
||||
cached,
|
||||
query,
|
||||
answer,
|
||||
appId,
|
||||
messageId,
|
||||
annotationId,
|
||||
onAdded,
|
||||
onEdit,
|
||||
onRemoved,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const isAnnotationFull = (enableBilling && plan.usage.annotatedResponse >= plan.total.annotatedResponse)
|
||||
const { setShowAnnotationFullModal } = useModalContext()
|
||||
const [showModal, setShowModal] = useState(false)
|
||||
const cachedBtnRef = useRef<HTMLDivElement>(null)
|
||||
const isCachedBtnHovering = useHover(cachedBtnRef)
|
||||
const handleAdd = async () => {
|
||||
if (isAnnotationFull) {
|
||||
setShowAnnotationFullModal()
|
||||
return
|
||||
}
|
||||
const res: any = await addAnnotation(appId, {
|
||||
message_id: messageId,
|
||||
question: query,
|
||||
answer,
|
||||
})
|
||||
Toast.notify({
|
||||
message: t('common.api.actionSuccess') as string,
|
||||
type: 'success',
|
||||
})
|
||||
onAdded(res.id, res.account?.name)
|
||||
}
|
||||
|
||||
const handleRemove = async () => {
|
||||
await delAnnotation(appId, annotationId!)
|
||||
Toast.notify({
|
||||
message: t('common.api.actionSuccess') as string,
|
||||
type: 'success',
|
||||
})
|
||||
onRemoved()
|
||||
setShowModal(false)
|
||||
}
|
||||
return (
|
||||
<div className={cn(className, 'inline-block')}>
|
||||
<div className='inline-flex p-0.5 space-x-0.5 rounded-lg bg-white border border-gray-100 shadow-md text-gray-500 cursor-pointer'>
|
||||
{cached
|
||||
? (
|
||||
<div>
|
||||
<div
|
||||
ref={cachedBtnRef}
|
||||
className={cn(isCachedBtnHovering ? 'bg-[#FEF3F2] text-[#D92D20]' : 'bg-[#EEF4FF] text-[#444CE7]', 'flex p-1 space-x-1 items-center rounded-md leading-4 text-xs font-medium')}
|
||||
onClick={() => setShowModal(true)}
|
||||
>
|
||||
{!isCachedBtnHovering
|
||||
? (
|
||||
<>
|
||||
<MessageFast className='w-4 h-4' />
|
||||
<div>{t('appDebug.feature.annotation.cached')}</div>
|
||||
</>
|
||||
)
|
||||
: <>
|
||||
<MessageCheckRemove className='w-4 h-4' />
|
||||
<div>{t('appDebug.feature.annotation.remove')}</div>
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<TooltipPlus
|
||||
popupContent={t('appDebug.feature.annotation.add') as string}
|
||||
>
|
||||
<div
|
||||
className='p-1 rounded-md hover:bg-[#EEF4FF] hover:text-[#444CE7] cursor-pointer'
|
||||
onClick={handleAdd}
|
||||
>
|
||||
<MessageFastPlus className='w-4 h-4' />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
)}
|
||||
<TooltipPlus
|
||||
popupContent={t('appDebug.feature.annotation.edit') as string}
|
||||
>
|
||||
<div
|
||||
className='p-1 cursor-pointer rounded-md hover:bg-black/5'
|
||||
onClick={onEdit}
|
||||
>
|
||||
<Edit04 className='w-4 h-4' />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
|
||||
</div>
|
||||
<RemoveAnnotationConfirmModal
|
||||
isShow={showModal}
|
||||
onHide={() => setShowModal(false)}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(CacheCtrlBtn)
|
|
@ -0,0 +1,138 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ScoreSlider from '../score-slider'
|
||||
import { Item } from './config-param'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { ModelType } from '@/app/components/header/account-setting/model-page/declarations'
|
||||
import ModelSelector from '@/app/components/header/account-setting/model-page/model-selector/portal-select'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import type { AnnotationReplyConfig } from '@/models/debug'
|
||||
import { ANNOTATION_DEFAULT } from '@/config'
|
||||
|
||||
type Props = {
|
||||
appId: string
|
||||
isShow: boolean
|
||||
onHide: () => void
|
||||
onSave: (embeddingModel: {
|
||||
embedding_provider_name: string
|
||||
embedding_model_name: string
|
||||
}, score: number) => void
|
||||
isInit?: boolean
|
||||
annotationConfig: AnnotationReplyConfig
|
||||
}
|
||||
|
||||
const ConfigParamModal: FC<Props> = ({
|
||||
isShow,
|
||||
onHide: doHide,
|
||||
onSave,
|
||||
isInit,
|
||||
annotationConfig: oldAnnotationConfig,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
embeddingsDefaultModel,
|
||||
isEmbeddingsDefaultModelValid,
|
||||
} = useProviderContext()
|
||||
const [annotationConfig, setAnnotationConfig] = useState(oldAnnotationConfig)
|
||||
|
||||
const [isLoading, setLoading] = useState(false)
|
||||
const [embeddingModel, setEmbeddingModel] = useState(oldAnnotationConfig.embedding_model
|
||||
? {
|
||||
providerName: oldAnnotationConfig.embedding_model.embedding_provider_name,
|
||||
modelName: oldAnnotationConfig.embedding_model.embedding_model_name,
|
||||
}
|
||||
: (embeddingsDefaultModel
|
||||
? {
|
||||
providerName: embeddingsDefaultModel.model_provider.provider_name,
|
||||
modelName: embeddingsDefaultModel.model_name,
|
||||
}
|
||||
: undefined))
|
||||
const onHide = () => {
|
||||
if (!isLoading)
|
||||
doHide()
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!embeddingModel || !embeddingModel.modelName || (embeddingModel.modelName === embeddingsDefaultModel?.model_name && !isEmbeddingsDefaultModelValid)) {
|
||||
Toast.notify({
|
||||
message: t('common.modelProvider.embeddingModel.required'),
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
await onSave({
|
||||
embedding_provider_name: embeddingModel.providerName,
|
||||
embedding_model_name: embeddingModel.modelName,
|
||||
}, annotationConfig.score_threshold)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={onHide}
|
||||
className='!p-8 !pb-6 !mt-14 !max-w-none !w-[640px]'
|
||||
wrapperClassName='!z-50'
|
||||
>
|
||||
<div className='mb-2 text-xl font-semibold text-[#1D2939]'>
|
||||
{t(`appAnnotation.initSetup.${isInit ? 'title' : 'configTitle'}`)}
|
||||
</div>
|
||||
|
||||
<div className='space-y-2'>
|
||||
<Item
|
||||
title={t('appDebug.feature.annotation.scoreThreshold.title')}
|
||||
tooltip={t('appDebug.feature.annotation.scoreThreshold.description')}
|
||||
>
|
||||
<ScoreSlider
|
||||
className='mt-1'
|
||||
value={(annotationConfig.score_threshold || ANNOTATION_DEFAULT.score_threshold) * 100}
|
||||
onChange={(val) => {
|
||||
setAnnotationConfig({
|
||||
...annotationConfig,
|
||||
score_threshold: val / 100,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Item>
|
||||
|
||||
<Item
|
||||
title={t('common.modelProvider.embeddingModel.key')}
|
||||
tooltip={t('appAnnotation.embeddingModelSwitchTip')}
|
||||
>
|
||||
<div className='pt-1'>
|
||||
<ModelSelector
|
||||
widthSameToTrigger
|
||||
value={embeddingModel as any}
|
||||
modelType={ModelType.embeddings}
|
||||
onChange={(val) => {
|
||||
setEmbeddingModel({
|
||||
providerName: val.model_provider.provider_name,
|
||||
modelName: val.model_name,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Item>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 flex gap-2 justify-end'>
|
||||
<Button onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
<Button
|
||||
type='primary'
|
||||
onClick={handleSave}
|
||||
className='flex items-center border-[0.5px]'
|
||||
loading={isLoading}
|
||||
>
|
||||
<div></div>
|
||||
<div>{t(`appAnnotation.initSetup.${isInit ? 'confirmBtn' : 'configConfirmBtn'}`)}</div>
|
||||
</Button >
|
||||
</div >
|
||||
</Modal >
|
||||
)
|
||||
}
|
||||
export default React.memo(ConfigParamModal)
|
|
@ -0,0 +1,124 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import ConfigParamModal from './config-param-modal'
|
||||
import Panel from '@/app/components/app/configuration/base/feature-panel'
|
||||
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import { HelpCircle, LinkExternal02, Settings04 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
import type { EmbeddingModelConfig } from '@/app/components/app/annotation/type'
|
||||
import { updateAnnotationScore } from '@/service/annotation'
|
||||
|
||||
type Props = {
|
||||
onEmbeddingChange: (embeddingModel: EmbeddingModelConfig) => void
|
||||
onScoreChange: (score: number, embeddingModel?: EmbeddingModelConfig) => void
|
||||
}
|
||||
|
||||
export const Item: FC<{ title: string; tooltip: string; children: JSX.Element }> = ({
|
||||
title,
|
||||
tooltip,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className='flex items-center space-x-1'>
|
||||
<div>{title}</div>
|
||||
<TooltipPlus
|
||||
popupContent={
|
||||
<div className='max-w-[200px] leading-[18px] text-[13px] font-medium text-gray-800'>{tooltip}</div>
|
||||
}
|
||||
>
|
||||
<HelpCircle className='w-3.5 h-3.5 text-gray-400' />
|
||||
</TooltipPlus>
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const AnnotationReplyConfig: FC<Props> = ({
|
||||
onEmbeddingChange,
|
||||
onScoreChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const matched = pathname.match(/\/app\/([^/]+)/)
|
||||
const appId = (matched?.length && matched[1]) ? matched[1] : ''
|
||||
const {
|
||||
annotationConfig,
|
||||
} = useContext(ConfigContext)
|
||||
|
||||
const [isShowEdit, setIsShowEdit] = React.useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel
|
||||
className="mt-4"
|
||||
headerIcon={
|
||||
<MessageFast className='w-4 h-4 text-[#444CE7]' />
|
||||
}
|
||||
title={t('appDebug.feature.annotation.title')}
|
||||
headerRight={
|
||||
<div className='flex items-center'>
|
||||
<div
|
||||
className='flex items-center rounded-md h-7 px-3 space-x-1 text-gray-700 cursor-pointer hover:bg-gray-200'
|
||||
onClick={() => { setIsShowEdit(true) }}
|
||||
>
|
||||
<Settings04 className="w-[14px] h-[14px]" />
|
||||
<div className='text-xs font-medium'>
|
||||
|
||||
{t('common.operation.params')}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className='ml-1 flex items-center h-7 px-3 space-x-1 leading-[18px] text-xs font-medium text-gray-700 rounded-md cursor-pointer hover:bg-gray-200'
|
||||
onClick={() => {
|
||||
router.push(`/app/${appId}/annotations`)
|
||||
}}>
|
||||
<div>{t('appDebug.feature.annotation.cacheManagement')}</div>
|
||||
<LinkExternal02 className='w-3.5 h-3.5' />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
noBodySpacing
|
||||
/>
|
||||
{isShowEdit && (
|
||||
<ConfigParamModal
|
||||
appId={appId}
|
||||
isShow
|
||||
onHide={() => {
|
||||
setIsShowEdit(false)
|
||||
}}
|
||||
onSave={async (embeddingModel, score) => {
|
||||
let isEmbeddingModelChanged = false
|
||||
if (
|
||||
embeddingModel.embedding_model_name !== annotationConfig.embedding_model.embedding_model_name
|
||||
&& embeddingModel.embedding_provider_name !== annotationConfig.embedding_model.embedding_provider_name
|
||||
) {
|
||||
await onEmbeddingChange(embeddingModel)
|
||||
isEmbeddingModelChanged = true
|
||||
}
|
||||
|
||||
if (score !== annotationConfig.score_threshold) {
|
||||
await updateAnnotationScore(appId, annotationConfig.id, score)
|
||||
if (isEmbeddingModelChanged)
|
||||
onScoreChange(score, embeddingModel)
|
||||
|
||||
else
|
||||
onScoreChange(score)
|
||||
}
|
||||
|
||||
setIsShowEdit(false)
|
||||
}}
|
||||
annotationConfig={annotationConfig}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default React.memo(AnnotationReplyConfig)
|
|
@ -0,0 +1,4 @@
|
|||
export enum PageType {
|
||||
log = 'log',
|
||||
annotation = 'annotation',
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import React, { useState } from 'react'
|
||||
import produce from 'immer'
|
||||
import type { AnnotationReplyConfig } from '@/models/debug'
|
||||
import { queryAnnotationJobStatus, updateAnnotationStatus } from '@/service/annotation'
|
||||
import type { EmbeddingModelConfig } from '@/app/components/app/annotation/type'
|
||||
import { AnnotationEnableStatus, JobStatus } from '@/app/components/app/annotation/type'
|
||||
import { sleep } from '@/utils'
|
||||
import { ANNOTATION_DEFAULT } from '@/config'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
type Params = {
|
||||
appId: string
|
||||
annotationConfig: AnnotationReplyConfig
|
||||
setAnnotationConfig: (annotationConfig: AnnotationReplyConfig) => void
|
||||
}
|
||||
const useAnnotationConfig = ({
|
||||
appId,
|
||||
annotationConfig,
|
||||
setAnnotationConfig,
|
||||
}: Params) => {
|
||||
const { plan, enableBilling } = useProviderContext()
|
||||
const isAnnotationFull = (enableBilling && plan.usage.annotatedResponse >= plan.total.annotatedResponse)
|
||||
const [isShowAnnotationFullModal, setIsShowAnnotationFullModal] = useState(false)
|
||||
const [isShowAnnotationConfigInit, doSetIsShowAnnotationConfigInit] = React.useState(false)
|
||||
const setIsShowAnnotationConfigInit = (isShow: boolean) => {
|
||||
if (isShow) {
|
||||
if (isAnnotationFull) {
|
||||
setIsShowAnnotationFullModal(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
doSetIsShowAnnotationConfigInit(isShow)
|
||||
}
|
||||
const ensureJobCompleted = async (jobId: string, status: AnnotationEnableStatus) => {
|
||||
let isCompleted = false
|
||||
while (!isCompleted) {
|
||||
const res: any = await queryAnnotationJobStatus(appId, status, jobId)
|
||||
isCompleted = res.job_status === JobStatus.completed
|
||||
if (isCompleted)
|
||||
break
|
||||
|
||||
await sleep(2000)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnableAnnotation = async (embeddingModel: EmbeddingModelConfig, score?: number) => {
|
||||
if (isAnnotationFull)
|
||||
return
|
||||
|
||||
const { job_id: jobId }: any = await updateAnnotationStatus(appId, AnnotationEnableStatus.enable, embeddingModel, score)
|
||||
await ensureJobCompleted(jobId, AnnotationEnableStatus.enable)
|
||||
setAnnotationConfig(produce(annotationConfig, (draft: AnnotationReplyConfig) => {
|
||||
draft.enabled = true
|
||||
draft.embedding_model = embeddingModel
|
||||
if (!draft.score_threshold)
|
||||
draft.score_threshold = ANNOTATION_DEFAULT.score_threshold
|
||||
}))
|
||||
}
|
||||
|
||||
const setScore = (score: number, embeddingModel?: EmbeddingModelConfig) => {
|
||||
setAnnotationConfig(produce(annotationConfig, (draft: AnnotationReplyConfig) => {
|
||||
draft.score_threshold = score
|
||||
if (embeddingModel)
|
||||
draft.embedding_model = embeddingModel
|
||||
}))
|
||||
}
|
||||
|
||||
const handleDisableAnnotation = async (embeddingModel: EmbeddingModelConfig) => {
|
||||
if (!annotationConfig.enabled)
|
||||
return
|
||||
|
||||
await updateAnnotationStatus(appId, AnnotationEnableStatus.disable, embeddingModel)
|
||||
setAnnotationConfig(produce(annotationConfig, (draft: AnnotationReplyConfig) => {
|
||||
draft.enabled = false
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
handleEnableAnnotation,
|
||||
handleDisableAnnotation,
|
||||
isShowAnnotationConfigInit,
|
||||
setIsShowAnnotationConfigInit,
|
||||
isShowAnnotationFullModal,
|
||||
setIsShowAnnotationFullModal,
|
||||
setScore,
|
||||
}
|
||||
}
|
||||
|
||||
export default useAnnotationConfig
|
|
@ -5,12 +5,22 @@ import React from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import GroupName from '../base/group-name'
|
||||
import Moderation from './moderation'
|
||||
import Annotation from './annotation/config-param'
|
||||
import type { EmbeddingModelConfig } from '@/app/components/app/annotation/type'
|
||||
|
||||
export type ToolboxProps = {
|
||||
showModerationSettings: boolean
|
||||
showAnnotation: boolean
|
||||
onEmbeddingChange: (embeddingModel: EmbeddingModelConfig) => void
|
||||
onScoreChange: (score: number, embeddingModel?: EmbeddingModelConfig) => void
|
||||
}
|
||||
|
||||
const Toolbox: FC<ToolboxProps> = ({ showModerationSettings }) => {
|
||||
const Toolbox: FC<ToolboxProps> = ({
|
||||
showModerationSettings,
|
||||
showAnnotation,
|
||||
onEmbeddingChange,
|
||||
onScoreChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
|
@ -21,6 +31,14 @@ const Toolbox: FC<ToolboxProps> = ({ showModerationSettings }) => {
|
|||
<Moderation />
|
||||
)
|
||||
}
|
||||
{
|
||||
(showAnnotation || true) && (
|
||||
<Annotation
|
||||
onEmbeddingChange={onEmbeddingChange}
|
||||
onScoreChange={onScoreChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import ReactSlider from 'react-slider'
|
||||
import cn from 'classnames'
|
||||
import s from './style.module.css'
|
||||
|
||||
type ISliderProps = {
|
||||
className?: string
|
||||
value: number
|
||||
max?: number
|
||||
min?: number
|
||||
step?: number
|
||||
disabled?: boolean
|
||||
onChange: (value: number) => void
|
||||
}
|
||||
|
||||
const Slider: React.FC<ISliderProps> = ({ className, max, min, step, value, disabled, onChange }) => {
|
||||
return <ReactSlider
|
||||
disabled={disabled}
|
||||
value={isNaN(value) ? 0 : value}
|
||||
min={min || 0}
|
||||
max={max || 100}
|
||||
step={step || 1}
|
||||
className={cn(className, s.slider)}
|
||||
thumbClassName={cn(s['slider-thumb'], 'top-[-7px] w-2 h-[18px] bg-white border !border-black/8 rounded-[36px] shadow-md cursor-pointer')}
|
||||
trackClassName={s['slider-track']}
|
||||
onChange={onChange}
|
||||
renderThumb={(props, state) => (
|
||||
<div {...props}>
|
||||
<div className='relative w-full h-full'>
|
||||
<div className='absolute top-[-16px] left-[50%] translate-x-[-50%] leading-[18px] text-xs font-medium text-gray-900'>
|
||||
{(state.valueNow / 100).toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
|
||||
export default Slider
|
|
@ -0,0 +1,20 @@
|
|||
.slider {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.slider.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.slider-thumb:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.slider-track {
|
||||
background-color: #528BFF;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.slider-track-1 {
|
||||
background-color: #E5E7EB;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Slider from '@/app/components/app/configuration/toolbox/score-slider/base-slider'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
value: number
|
||||
onChange: (value: number) => void
|
||||
}
|
||||
|
||||
const ScoreSlider: FC<Props> = ({
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className='h-[1px] mt-[14px]'>
|
||||
<Slider
|
||||
max={100}
|
||||
min={80}
|
||||
step={1}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-[10px] flex justify-between items-center leading-4 text-xs font-normal '>
|
||||
<div className='flex space-x-1 text-[#00A286]'>
|
||||
<div>0.8</div>
|
||||
<div>·</div>
|
||||
<div>{t('appDebug.feature.annotation.scoreThreshold.easyMatch')}</div>
|
||||
</div>
|
||||
<div className='flex space-x-1 text-[#0057D8]'>
|
||||
<div>1.0</div>
|
||||
<div>·</div>
|
||||
<div>{t('appDebug.feature.annotation.scoreThreshold.accurateMatch')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(ScoreSlider)
|
45
web/app/components/app/log-annotation/index.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Log from '@/app/components/app/log'
|
||||
import Annotation from '@/app/components/app/annotation'
|
||||
import { PageType } from '@/app/components/app/configuration/toolbox/annotation/type'
|
||||
import TabSlider from '@/app/components/base/tab-slider-plain'
|
||||
|
||||
type Props = {
|
||||
pageType: PageType
|
||||
appId: string
|
||||
}
|
||||
|
||||
const LogAnnotation: FC<Props> = ({
|
||||
pageType,
|
||||
appId,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
|
||||
const options = [
|
||||
{ value: PageType.log, text: t('appLog.title') },
|
||||
{ value: PageType.annotation, text: t('appAnnotation.title') },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className='pt-4 px-6 h-full flex flex-col'>
|
||||
<TabSlider
|
||||
className='shrink-0'
|
||||
value={pageType}
|
||||
onChange={(value) => {
|
||||
router.push(`/app/${appId}/${value === PageType.log ? 'logs' : 'annotations'}`)
|
||||
}}
|
||||
options={options}
|
||||
/>
|
||||
<div className='mt-3 grow'>
|
||||
{pageType === PageType.log && (<Log appId={appId} />)}
|
||||
{pageType === PageType.annotation && (<Annotation appId={appId} />)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(LogAnnotation)
|
|
@ -15,7 +15,7 @@ import s from './style.module.css'
|
|||
import Loading from '@/app/components/base/loading'
|
||||
import { fetchChatConversations, fetchCompletionConversations } from '@/service/log'
|
||||
import { fetchAppDetail } from '@/service/apps'
|
||||
|
||||
import { APP_PAGE_LIMIT } from '@/config'
|
||||
export type ILogsProps = {
|
||||
appId: string
|
||||
}
|
||||
|
@ -26,9 +26,6 @@ export type QueryParam = {
|
|||
keyword?: string
|
||||
}
|
||||
|
||||
// Custom page count is not currently supported.
|
||||
const limit = 10
|
||||
|
||||
const ThreeDotsIcon = ({ className }: SVGProps<SVGElement>) => {
|
||||
return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
||||
<path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
|
@ -60,7 +57,7 @@ const Logs: FC<ILogsProps> = ({ appId }) => {
|
|||
|
||||
const query = {
|
||||
page: currPage + 1,
|
||||
limit,
|
||||
limit: APP_PAGE_LIMIT,
|
||||
...(queryParams.period !== 'all'
|
||||
? {
|
||||
start: dayjs().subtract(queryParams.period as number, 'day').startOf('day').format('YYYY-MM-DD HH:mm'),
|
||||
|
@ -93,11 +90,8 @@ const Logs: FC<ILogsProps> = ({ appId }) => {
|
|||
|
||||
return (
|
||||
<div className='flex flex-col h-full'>
|
||||
<div className='flex flex-col justify-center px-6 pt-4'>
|
||||
<h1 className='flex text-xl font-medium text-gray-900'>{t('appLog.title')}</h1>
|
||||
<p className='flex text-sm font-normal text-gray-500'>{t('appLog.description')}</p>
|
||||
</div>
|
||||
<div className='flex flex-col px-6 py-4 flex-1'>
|
||||
<p className='flex text-sm font-normal text-gray-500'>{t('appLog.description')}</p>
|
||||
<div className='flex flex-col py-4 flex-1'>
|
||||
<Filter appId={appId} queryParams={queryParams} setQueryParams={setQueryParams} />
|
||||
{total === undefined
|
||||
? <Loading type='app' />
|
||||
|
@ -106,14 +100,14 @@ const Logs: FC<ILogsProps> = ({ appId }) => {
|
|||
: <EmptyElement appUrl={`${appDetail?.site.app_base_url}/${appDetail?.mode}/${appDetail?.site.access_token}`} />
|
||||
}
|
||||
{/* Show Pagination only if the total is more than the limit */}
|
||||
{(total && total > limit)
|
||||
{(total && total > APP_PAGE_LIMIT)
|
||||
? <Pagination
|
||||
className="flex items-center w-full h-10 text-sm select-none mt-8"
|
||||
currentPage={currPage}
|
||||
edgePageCount={2}
|
||||
middlePagesSiblingCount={1}
|
||||
setCurrentPage={setCurrPage}
|
||||
totalPages={Math.ceil(total / limit)}
|
||||
totalPages={Math.ceil(total / APP_PAGE_LIMIT)}
|
||||
truncableClassName="w-8 px-0.5 text-center"
|
||||
truncableText="..."
|
||||
>
|
||||
|
@ -131,8 +125,8 @@ const Logs: FC<ILogsProps> = ({ appId }) => {
|
|||
/>
|
||||
</div>
|
||||
<Pagination.NextButton
|
||||
disabled={currPage === Math.ceil(total / limit) - 1}
|
||||
className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === Math.ceil(total / limit) - 1 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} >
|
||||
disabled={currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1}
|
||||
className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} >
|
||||
{t('appLog.table.pagination.next')}
|
||||
<ArrowRightIcon className="ml-3 h-3 w-3" />
|
||||
</Pagination.NextButton>
|
||||
|
|
|
@ -34,6 +34,7 @@ import ModelIcon from '@/app/components/app/configuration/config-model/model-ico
|
|||
import ModelName from '@/app/components/app/configuration/config-model/model-name'
|
||||
import ModelModeTypeLabel from '@/app/components/app/configuration/config-model/model-mode-type-label'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import TextGeneration from '@/app/components/app/text-generate/item'
|
||||
|
||||
type IConversationList = {
|
||||
logs?: ChatConversationsResponse | CompletionConversationsResponse
|
||||
|
@ -83,7 +84,6 @@ const getFormattedChatList = (messages: ChatMessage[]) => {
|
|||
log: item.message as any,
|
||||
message_files: item.message_files,
|
||||
})
|
||||
|
||||
newChatList.push({
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
|
@ -96,7 +96,26 @@ const getFormattedChatList = (messages: ChatMessage[]) => {
|
|||
tokens: item.answer_tokens + item.message_tokens,
|
||||
latency: item.provider_response_latency.toFixed(2),
|
||||
},
|
||||
annotation: item.annotation,
|
||||
annotation: (() => {
|
||||
if (item.annotation_hit_history) {
|
||||
return {
|
||||
id: item.annotation_hit_history.annotation_id,
|
||||
authorName: item.annotation_hit_history.annotation_create_account.name,
|
||||
created_at: item.annotation_hit_history.created_at,
|
||||
}
|
||||
}
|
||||
|
||||
if (item.annotation) {
|
||||
return {
|
||||
id: '',
|
||||
authorName: '',
|
||||
logAnnotation: item.annotation,
|
||||
created_at: 0,
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
})(),
|
||||
})
|
||||
})
|
||||
return newChatList
|
||||
|
@ -253,14 +272,26 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
|||
)}
|
||||
|
||||
{!isChatMode
|
||||
? <div className="px-2.5 py-4">
|
||||
<Chat
|
||||
chatList={getFormattedChatList([detail.message])}
|
||||
isHideSendInput={true}
|
||||
onFeedback={onFeedback}
|
||||
onSubmitAnnotation={onSubmitAnnotation}
|
||||
displayScene='console'
|
||||
isShowPromptLog
|
||||
? <div className="px-6 py-4">
|
||||
<div className='flex h-[18px] items-center space-x-3'>
|
||||
<div className='leading-[18px] text-xs font-semibold text-gray-500 uppercase'>{t('appLog.table.header.output')}</div>
|
||||
<div className='grow h-[1px]' style={{
|
||||
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, rgb(243, 244, 246) 100%)',
|
||||
}}></div>
|
||||
</div>
|
||||
<TextGeneration
|
||||
className='mt-2'
|
||||
content={detail.message.answer}
|
||||
messageId={detail.message.id}
|
||||
isError={false}
|
||||
onRetry={() => { }}
|
||||
isInstalledApp={false}
|
||||
supportFeedback
|
||||
feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
|
||||
onFeedback={feedback => onFeedback(detail.message.id, feedback)}
|
||||
supportAnnotation
|
||||
appId={appDetail?.id}
|
||||
varList={varList}
|
||||
/>
|
||||
</div>
|
||||
: items.length < 8
|
||||
|
@ -269,9 +300,11 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
|||
chatList={items}
|
||||
isHideSendInput={true}
|
||||
onFeedback={onFeedback}
|
||||
onSubmitAnnotation={onSubmitAnnotation}
|
||||
displayScene='console'
|
||||
isShowPromptLog
|
||||
supportAnnotation
|
||||
appId={appDetail?.id}
|
||||
onChatListChange={setItems}
|
||||
/>
|
||||
</div>
|
||||
: <div
|
||||
|
@ -309,7 +342,6 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
|||
chatList={items}
|
||||
isHideSendInput={true}
|
||||
onFeedback={onFeedback}
|
||||
onSubmitAnnotation={onSubmitAnnotation}
|
||||
displayScene='console'
|
||||
isShowPromptLog
|
||||
/>
|
||||
|
@ -427,7 +459,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
|||
<Tooltip
|
||||
htmlContent={
|
||||
<span className='text-xs text-gray-500 inline-flex items-center'>
|
||||
<EditIconSolid className='mr-1' />{`${t('appLog.detail.annotationTip', { user: annotation?.account?.name })} ${dayjs.unix(annotation?.created_at || dayjs().unix()).format('MM-DD hh:mm A')}`}
|
||||
<EditIconSolid className='mr-1' />{`${t('appLog.detail.annotationTip', { user: annotation?.logAnnotation?.account?.name })} ${dayjs.unix(annotation?.created_at || dayjs().unix()).format('MM-DD hh:mm A')}`}
|
||||
</span>
|
||||
}
|
||||
className={(isHighlight && !isChatMode) ? '' : '!hidden'}
|
||||
|
|
|
@ -19,6 +19,8 @@ import { Bookmark } from '@/app/components/base/icons/src/vender/line/general'
|
|||
import { Stars02 } from '@/app/components/base/icons/src/vender/line/weather'
|
||||
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { fetchTextGenerationMessge } from '@/service/debug'
|
||||
import AnnotationCtrlBtn from '@/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn'
|
||||
import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
|
||||
|
||||
const MAX_DEPTH = 3
|
||||
export type IGenerationItemProps = {
|
||||
|
@ -41,6 +43,10 @@ export type IGenerationItemProps = {
|
|||
installedAppId?: string
|
||||
taskId?: string
|
||||
controlClearMoreLikeThis?: number
|
||||
supportFeedback?: boolean
|
||||
supportAnnotation?: boolean
|
||||
appId?: string
|
||||
varList?: { label: string; value: string | number | object }[]
|
||||
}
|
||||
|
||||
export const SimpleBtn = ({ className, isDisabled, onClick, children }: {
|
||||
|
@ -82,6 +88,10 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
|||
installedAppId,
|
||||
taskId,
|
||||
controlClearMoreLikeThis,
|
||||
supportFeedback,
|
||||
supportAnnotation,
|
||||
appId,
|
||||
varList,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const params = useParams()
|
||||
|
@ -100,6 +110,8 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
|||
setChildFeedback(childFeedback)
|
||||
}
|
||||
|
||||
const [isShowReplyModal, setIsShowReplyModal] = useState(false)
|
||||
const question = (varList && varList?.length > 0) ? varList?.map(({ label, value }) => `${label}:${value}`).join('&') : ''
|
||||
const [isQuerying, { setTrue: startQuerying, setFalse: stopQuerying }] = useBoolean(false)
|
||||
|
||||
const childProps = {
|
||||
|
@ -168,6 +180,57 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
|||
setModal(true)
|
||||
}
|
||||
|
||||
const ratingContent = (
|
||||
<>
|
||||
{!isError && messageId && !feedback?.rating && (
|
||||
<SimpleBtn className="!px-0">
|
||||
<>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback?.({
|
||||
rating: 'like',
|
||||
})
|
||||
}}
|
||||
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
|
||||
<HandThumbUpIcon width={16} height={16} />
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback?.({
|
||||
rating: 'dislike',
|
||||
})
|
||||
}}
|
||||
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
|
||||
<HandThumbDownIcon width={16} height={16} />
|
||||
</div>
|
||||
</>
|
||||
</SimpleBtn>
|
||||
)}
|
||||
{!isError && messageId && feedback?.rating === 'like' && (
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback?.({
|
||||
rating: null,
|
||||
})
|
||||
}}
|
||||
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-primary-600 border border-primary-200 bg-primary-100 hover:border-primary-300 hover:bg-primary-200'>
|
||||
<HandThumbUpIcon width={16} height={16} />
|
||||
</div>
|
||||
)}
|
||||
{!isError && messageId && feedback?.rating === 'dislike' && (
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback?.({
|
||||
rating: null,
|
||||
})
|
||||
}}
|
||||
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-red-600 border border-red-200 bg-red-100 hover:border-red-300 hover:bg-red-200'>
|
||||
<HandThumbDownIcon width={16} height={16} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn(className, isTop ? `rounded-xl border ${!isError ? 'border-gray-200 bg-white' : 'border-[#FECDCA] bg-[#FEF3F2]'} ` : 'rounded-br-xl !mt-0')}
|
||||
style={isTop
|
||||
|
@ -196,7 +259,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
|||
{isError
|
||||
? <div className='text-gray-400 text-sm'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
|
||||
: (
|
||||
<Markdown content={ content } />
|
||||
<Markdown content={content} />
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
@ -214,7 +277,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
|||
showModal => (
|
||||
<SimpleBtn
|
||||
isDisabled={isError || !messageId}
|
||||
className={cn(isMobile && '!px-1.5', 'space-x-1 mr-2')}
|
||||
className={cn(isMobile && '!px-1.5', 'space-x-1 mr-1')}
|
||||
onClick={() => handleOpenLogModal(showModal)}>
|
||||
<File02 className='w-3.5 h-3.5' />
|
||||
{!isMobile && <div>{t('common.operation.log')}</div>}
|
||||
|
@ -261,54 +324,50 @@ const GenerationItem: FC<IGenerationItemProps> = ({
|
|||
{!isMobile && <div>{t('share.generation.batchFailed.retry')}</div>}
|
||||
</SimpleBtn>}
|
||||
{!isError && messageId && <div className="mx-3 w-[1px] h-[14px] bg-gray-200"></div>}
|
||||
{!isError && messageId && !feedback?.rating && (
|
||||
<SimpleBtn className="!px-0">
|
||||
<>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback?.({
|
||||
rating: 'like',
|
||||
})
|
||||
}}
|
||||
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
|
||||
<HandThumbUpIcon width={16} height={16} />
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback?.({
|
||||
rating: 'dislike',
|
||||
})
|
||||
}}
|
||||
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
|
||||
<HandThumbDownIcon width={16} height={16} />
|
||||
</div>
|
||||
</>
|
||||
</SimpleBtn>
|
||||
)}
|
||||
{!isError && messageId && feedback?.rating === 'like' && (
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback?.({
|
||||
rating: null,
|
||||
})
|
||||
}}
|
||||
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-primary-600 border border-primary-200 bg-primary-100 hover:border-primary-300 hover:bg-primary-200'>
|
||||
<HandThumbUpIcon width={16} height={16} />
|
||||
</div>
|
||||
)}
|
||||
{!isError && messageId && feedback?.rating === 'dislike' && (
|
||||
<div
|
||||
onClick={() => {
|
||||
onFeedback?.({
|
||||
rating: null,
|
||||
})
|
||||
}}
|
||||
className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-red-600 border border-red-200 bg-red-100 hover:border-red-300 hover:bg-red-200'>
|
||||
<HandThumbDownIcon width={16} height={16} />
|
||||
</div>
|
||||
)}
|
||||
{ratingContent}
|
||||
</>
|
||||
)}
|
||||
|
||||
{supportAnnotation && (
|
||||
<>
|
||||
<div className='ml-2 mr-1 h-[14px] w-[1px] bg-gray-200'></div>
|
||||
<AnnotationCtrlBtn
|
||||
appId={appId!}
|
||||
messageId={messageId!}
|
||||
className='ml-1'
|
||||
query={question}
|
||||
answer={content}
|
||||
// not support cache. So can not be cached
|
||||
cached={false}
|
||||
onAdded={() => {
|
||||
|
||||
}}
|
||||
onEdit={() => setIsShowReplyModal(true)}
|
||||
onRemoved={() => { }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<EditReplyModal
|
||||
appId={appId!}
|
||||
messageId={messageId!}
|
||||
isShow={isShowReplyModal}
|
||||
onHide={() => setIsShowReplyModal(false)}
|
||||
query={question}
|
||||
answer={content}
|
||||
onAdded={() => { }}
|
||||
onEdited={() => { }}
|
||||
createdAt={0}
|
||||
onRemove={() => { }}
|
||||
onlyEditResponse
|
||||
/>
|
||||
|
||||
{supportFeedback && (
|
||||
<div className='ml-1'>
|
||||
{ratingContent}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
|
||||
</div>
|
||||
|
|
69
web/app/components/base/drawer-plus/index.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useRef } from 'react'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
|
||||
type Props = {
|
||||
isShow: boolean
|
||||
onHide: () => void
|
||||
maxWidthClassName?: string
|
||||
height?: number | string
|
||||
title: string | JSX.Element
|
||||
body: JSX.Element
|
||||
foot?: JSX.Element
|
||||
}
|
||||
|
||||
const DrawerPlus: FC<Props> = ({
|
||||
isShow,
|
||||
onHide,
|
||||
maxWidthClassName = '!max-w-[640px]',
|
||||
height = 'calc(100vh - 72px)',
|
||||
title,
|
||||
body,
|
||||
foot,
|
||||
}) => {
|
||||
const ref = useRef(null)
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
if (!isShow)
|
||||
return null
|
||||
|
||||
return (
|
||||
// clickOutsideNotOpen to fix confirm modal click cause drawer close
|
||||
<Drawer isOpen={isShow} clickOutsideNotOpen onClose={onHide} footer={null} mask={isMobile} panelClassname={`mt-16 mx-2 sm:mr-2 mb-3 !p-0 ${maxWidthClassName} rounded-xl`}>
|
||||
<div
|
||||
className='w-full flex flex-col bg-white border-[0.5px] border-gray-200 rounded-xl shadow-xl'
|
||||
style={{
|
||||
height,
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<div className='shrink-0 flex justify-between items-center pl-6 pr-5 h-14 border-b border-b-gray-100'>
|
||||
<div className='text-base font-semibold text-gray-900'>
|
||||
{title}
|
||||
</div>
|
||||
<div className='flex items-center'>
|
||||
<div
|
||||
onClick={onHide}
|
||||
className='flex justify-center items-center w-6 h-6 cursor-pointer'
|
||||
>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='grow overflow-y-auto'>
|
||||
{body}
|
||||
</div>
|
||||
{foot && (
|
||||
<div className='shrink-0'>
|
||||
{foot}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
export default React.memo(DrawerPlus)
|
|
@ -14,6 +14,7 @@ export type IDrawerProps = {
|
|||
isOpen: boolean
|
||||
// closable: boolean
|
||||
showClose?: boolean
|
||||
clickOutsideNotOpen?: boolean
|
||||
onClose: () => void
|
||||
onCancel?: () => void
|
||||
onOk?: () => void
|
||||
|
@ -28,6 +29,7 @@ export default function Drawer({
|
|||
mask = true,
|
||||
showClose = false,
|
||||
isOpen,
|
||||
clickOutsideNotOpen,
|
||||
onClose,
|
||||
onCancel,
|
||||
onOk,
|
||||
|
@ -37,7 +39,7 @@ export default function Drawer({
|
|||
<Dialog
|
||||
unmount={false}
|
||||
open={isOpen}
|
||||
onClose={() => onClose()}
|
||||
onClose={() => !clickOutsideNotOpen && onClose()}
|
||||
className="fixed z-30 inset-0 overflow-y-auto"
|
||||
>
|
||||
<div className="flex w-screen h-screen justify-end">
|
||||
|
|
11
web/app/components/base/icons/assets/public/avatar/robot.svg
Normal file
After Width: | Height: | Size: 36 KiB |
12
web/app/components/base/icons/assets/public/avatar/user.svg
Normal file
|
@ -0,0 +1,12 @@
|
|||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_5968_39205)">
|
||||
<rect width="512" height="512" rx="256" fill="#B2DDFF"/>
|
||||
<circle opacity="0.68" cx="256" cy="196" r="84" fill="white"/>
|
||||
<ellipse opacity="0.68" cx="256" cy="583.5" rx="266" ry="274.5" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_5968_39205">
|
||||
<rect width="512" height="512" rx="256" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 465 B |
|
@ -1,9 +1,9 @@
|
|||
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g clip-path="url(#clip0_1_382)">
|
||||
<rect width="600" height="600" fill="url(#pattern0)"/>
|
||||
<rect width="600" height="600" fill="url(#pattern999)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<pattern id="pattern999" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlink:href="#image0_1_382" transform="scale(0.000976562)"/>
|
||||
</pattern>
|
||||
<clipPath id="clip0_1_382">
|
||||
|
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 364 KiB |
|
@ -0,0 +1,5 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="message-check-remove">
|
||||
<path id="Vector" d="M15.2 2.99994H7.8C6.11984 2.99994 5.27976 2.99994 4.63803 3.32693C4.07354 3.61455 3.6146 4.07349 3.32698 4.63797C3 5.27971 3 6.11979 3 7.79994V13.9999C3 14.9299 3 15.3949 3.10222 15.7764C3.37962 16.8117 4.18827 17.6203 5.22354 17.8977C5.60504 17.9999 6.07003 17.9999 7 17.9999V20.3354C7 20.8683 7 21.1347 7.10923 21.2716C7.20422 21.3906 7.34827 21.4598 7.50054 21.4596C7.67563 21.4594 7.88367 21.293 8.29976 20.9601L10.6852 19.0518C11.1725 18.6619 11.4162 18.467 11.6875 18.3284C11.9282 18.2054 12.1844 18.1155 12.4492 18.0612C12.7477 17.9999 13.0597 17.9999 13.6837 17.9999H16.2C17.8802 17.9999 18.7202 17.9999 19.362 17.673C19.9265 17.3853 20.3854 16.9264 20.673 16.3619C21 15.7202 21 14.8801 21 13.1999V8.79994M12.3333 13.4999L14 10.4999H10L11.6667 7.49994M19.2322 4.76771L21 2.99994M21 2.99994L22.7678 1.23218M21 2.99994L19.2322 1.23218M21 2.99994L22.7678 4.76771" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.2 3H7.8C6.11984 3 5.27976 3 4.63803 3.32698C4.07354 3.6146 3.6146 4.07354 3.32698 4.63803C3 5.27976 3 6.11984 3 7.8V14C3 14.93 3 15.395 3.10222 15.7765C3.37962 16.8117 4.18827 17.6204 5.22354 17.8978C5.60504 18 6.07003 18 7 18V20.3355C7 20.8684 7 21.1348 7.10923 21.2716C7.20422 21.3906 7.34827 21.4599 7.50054 21.4597C7.67563 21.4595 7.88367 21.2931 8.29976 20.9602L10.6852 19.0518C11.1725 18.662 11.4162 18.4671 11.6875 18.3285C11.9282 18.2055 12.1844 18.1156 12.4492 18.0613C12.7477 18 13.0597 18 13.6837 18H16.2C17.8802 18 18.7202 18 19.362 17.673C19.9265 17.3854 20.3854 16.9265 20.673 16.362C21 15.7202 21 14.8802 21 13.2V8.8M12.3333 13.5L14 10.5H10L11.6667 7.5M21 5V3M21 3V1M21 3H19M21 3H23" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 896 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 12.5V6.8C20 5.11984 20 4.27976 19.673 3.63803C19.3854 3.07354 18.9265 2.6146 18.362 2.32698C17.7202 2 16.8802 2 15.2 2H8.8C7.11984 2 6.27976 2 5.63803 2.32698C5.07354 2.6146 4.6146 3.07354 4.32698 3.63803C4 4.27976 4 5.11984 4 6.8V17.2C4 18.8802 4 19.7202 4.32698 20.362C4.6146 20.9265 5.07354 21.3854 5.63803 21.673C6.27976 22 7.1198 22 8.79986 22H12.5M14 11H8M10 15H8M16 7H8M15 19L18 22M18 22L21 19M18 22V16" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 608 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 18L19.9999 19.094C19.4695 19.6741 18.7502 20 18.0002 20C17.2501 20 16.5308 19.6741 16.0004 19.094C15.4693 18.5151 14.75 18.1901 14.0002 18.1901C13.2504 18.1901 12.5312 18.5151 12 19.094M3.00003 20H4.67457C5.16376 20 5.40835 20 5.63852 19.9447C5.84259 19.8957 6.03768 19.8149 6.21663 19.7053C6.41846 19.5816 6.59141 19.4086 6.93732 19.0627L19.5001 6.49998C20.3285 5.67156 20.3285 4.32841 19.5001 3.49998C18.6716 2.67156 17.3285 2.67156 16.5001 3.49998L3.93729 16.0627C3.59139 16.4086 3.41843 16.5816 3.29475 16.7834C3.18509 16.9624 3.10428 17.1574 3.05529 17.3615C3.00003 17.5917 3.00003 17.8363 3.00003 18.3255V20Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 813 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22.7 11.5L20.7005 13.5L18.7 11.5M20.9451 13C20.9814 12.6717 21 12.338 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C14.8273 21 17.35 19.6963 19 17.6573M12 7V12L15 14" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 399 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.2414 2H7.7588C6.95383 1.99999 6.28946 1.99998 5.74827 2.04419C5.18617 2.09012 4.66947 2.18868 4.18413 2.43598C3.43149 2.81947 2.81956 3.43139 2.43607 4.18404C2.18878 4.66937 2.09022 5.18608 2.04429 5.74818C2.00007 6.28937 2.00008 6.95373 2.0001 7.7587L2.00005 14.1376C1.99962 14.933 1.9993 15.5236 2.13639 16.0353C2.50626 17.4156 3.58445 18.4938 4.96482 18.8637C5.27229 18.9461 5.60829 18.9789 6.0001 18.9918L6.00009 20.371C6.00005 20.6062 6 20.846 6.01785 21.0425C6.03492 21.2305 6.08012 21.5852 6.32778 21.8955C6.61276 22.2525 7.0449 22.4602 7.50172 22.4597C7.8987 22.4593 8.20394 22.273 8.36137 22.1689C8.52597 22.06 8.7132 21.9102 8.89688 21.7632L11.31 19.8327C11.8286 19.4178 11.9826 19.3007 12.1425 19.219C12.303 19.137 12.4738 19.0771 12.6504 19.0408C12.8263 19.0047 13.0197 19 13.6838 19H16.2414C17.0464 19 17.7107 19 18.2519 18.9558C18.814 18.9099 19.3307 18.8113 19.8161 18.564C20.5687 18.1805 21.1806 17.5686 21.5641 16.816C21.8114 16.3306 21.91 15.8139 21.9559 15.2518C22.0001 14.7106 22.0001 14.0463 22.0001 13.2413V7.75868C22.0001 6.95372 22.0001 6.28936 21.9559 5.74818C21.91 5.18608 21.8114 4.66937 21.5641 4.18404C21.1806 3.43139 20.5687 2.81947 19.8161 2.43598C19.3307 2.18868 18.814 2.09012 18.2519 2.04419C17.7107 1.99998 17.0464 1.99999 16.2414 2ZM12.681 5.5349C12.8938 5.61898 13.0218 5.83714 12.9916 6.06386L12.5688 9.23501L14.48 9.23501C14.5899 9.23498 14.7038 9.23496 14.7979 9.24356C14.8905 9.25203 15.0589 9.27446 15.2095 9.39066C15.3851 9.52617 15.4913 9.73269 15.4996 9.95432C15.5066 10.1444 15.427 10.2945 15.38 10.3747C15.3324 10.4563 15.2661 10.549 15.2022 10.6384L11.9072 15.2514C11.7743 15.4375 11.5317 15.5092 11.319 15.4251C11.1063 15.341 10.9782 15.1229 11.0084 14.8961L11.4312 11.725L9.52004 11.725C9.41011 11.725 9.29618 11.725 9.20206 11.7164C9.10948 11.708 8.94106 11.6855 8.79051 11.5693C8.61493 11.4338 8.50866 11.2273 8.50044 11.0057C8.49339 10.8156 8.57303 10.6655 8.61996 10.5853C8.66766 10.5037 8.7339 10.411 8.79781 10.3216L12.0928 5.70858C12.2257 5.52246 12.4683 5.45083 12.681 5.5349Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.6747 17.2619C22.0824 17.6345 22.1107 18.2671 21.7381 18.6747L20.738 19.7687C20.0284 20.5448 19.0458 21 18.0002 21C16.9549 21 15.9726 20.5452 15.2631 19.7696C14.9112 19.3863 14.4549 19.1901 14.0002 19.1901C13.5454 19.1901 13.0889 19.3864 12.7369 19.7701C12.3635 20.177 11.7309 20.2043 11.324 19.8309C10.917 19.4575 10.8898 18.8249 11.2632 18.418C11.9735 17.6438 12.9555 17.1901 14.0002 17.1901C15.045 17.1901 16.0269 17.6438 16.7373 18.418L16.7384 18.4192C17.0897 18.8034 17.5458 19 18.0002 19C18.4545 19 18.9106 18.8034 19.2618 18.4193L20.2619 17.3253C20.6346 16.9177 21.2671 16.8893 21.6747 17.2619Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.793 2.79287C17.0119 1.57393 18.9882 1.57392 20.2072 2.79287C21.4261 4.01183 21.4261 5.98814 20.2072 7.20709L7.64443 19.7698C7.62463 19.7896 7.60502 19.8093 7.58556 19.8288C7.29811 20.1168 7.04467 20.3707 6.73914 20.5579C6.47072 20.7224 6.17809 20.8436 5.87198 20.9171C5.52353 21.0007 5.16478 21.0004 4.75788 21C4.73034 21 4.70258 21 4.67458 21H3.00004C2.44776 21 2.00004 20.5523 2.00004 20V18.3255C2.00004 18.2975 2.00001 18.2697 1.99999 18.2422C1.99961 17.8353 1.99928 17.4765 2.08293 17.1281C2.15642 16.822 2.27763 16.5293 2.44212 16.2609C2.62936 15.9554 2.88327 15.7019 3.17125 15.4145C3.19075 15.395 3.2104 15.3754 3.23019 15.3556L15.793 2.79287Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
92
web/app/components/base/icons/src/public/avatar/Robot.json
Normal file
16
web/app/components/base/icons/src/public/avatar/Robot.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Robot.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Robot'
|
||||
|
||||
export default Icon
|
89
web/app/components/base/icons/src/public/avatar/User.json
Normal file
|
@ -0,0 +1,89 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "512",
|
||||
"height": "512",
|
||||
"viewBox": "0 0 512 512",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"clip-path": "url(#clip0_5968_39205)"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "rect",
|
||||
"attributes": {
|
||||
"width": "512",
|
||||
"height": "512",
|
||||
"rx": "256",
|
||||
"fill": "#B2DDFF"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "circle",
|
||||
"attributes": {
|
||||
"opacity": "0.68",
|
||||
"cx": "256",
|
||||
"cy": "196",
|
||||
"r": "84",
|
||||
"fill": "white"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "ellipse",
|
||||
"attributes": {
|
||||
"opacity": "0.68",
|
||||
"cx": "256",
|
||||
"cy": "583.5",
|
||||
"rx": "266",
|
||||
"ry": "274.5",
|
||||
"fill": "white"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "defs",
|
||||
"attributes": {},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "clipPath",
|
||||
"attributes": {
|
||||
"id": "clip0_5968_39205"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "rect",
|
||||
"attributes": {
|
||||
"width": "512",
|
||||
"height": "512",
|
||||
"rx": "256",
|
||||
"fill": "white"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "User"
|
||||
}
|
16
web/app/components/base/icons/src/public/avatar/User.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './User.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'User'
|
||||
|
||||
export default Icon
|
2
web/app/components/base/icons/src/public/avatar/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export { default as Robot } from './Robot'
|
||||
export { default as User } from './User'
|
|
@ -25,7 +25,7 @@
|
|||
"attributes": {
|
||||
"width": "600",
|
||||
"height": "600",
|
||||
"fill": "url(#pattern0)"
|
||||
"fill": "url(#pattern999)"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
|
@ -40,7 +40,7 @@
|
|||
"type": "element",
|
||||
"name": "pattern",
|
||||
"attributes": {
|
||||
"id": "pattern0",
|
||||
"id": "pattern999",
|
||||
"patternContentUnits": "objectBoundingBox",
|
||||
"width": "1",
|
||||
"height": "1"
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "g",
|
||||
"attributes": {
|
||||
"id": "message-check-remove"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector",
|
||||
"d": "M15.2 2.99994H7.8C6.11984 2.99994 5.27976 2.99994 4.63803 3.32693C4.07354 3.61455 3.6146 4.07349 3.32698 4.63797C3 5.27971 3 6.11979 3 7.79994V13.9999C3 14.9299 3 15.3949 3.10222 15.7764C3.37962 16.8117 4.18827 17.6203 5.22354 17.8977C5.60504 17.9999 6.07003 17.9999 7 17.9999V20.3354C7 20.8683 7 21.1347 7.10923 21.2716C7.20422 21.3906 7.34827 21.4598 7.50054 21.4596C7.67563 21.4594 7.88367 21.293 8.29976 20.9601L10.6852 19.0518C11.1725 18.6619 11.4162 18.467 11.6875 18.3284C11.9282 18.2054 12.1844 18.1155 12.4492 18.0612C12.7477 17.9999 13.0597 17.9999 13.6837 17.9999H16.2C17.8802 17.9999 18.7202 17.9999 19.362 17.673C19.9265 17.3853 20.3854 16.9264 20.673 16.3619C21 15.7202 21 14.8801 21 13.1999V8.79994M12.3333 13.4999L14 10.4999H10L11.6667 7.49994M19.2322 4.76771L21 2.99994M21 2.99994L22.7678 1.23218M21 2.99994L19.2322 1.23218M21 2.99994L22.7678 4.76771",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "MessageCheckRemove"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './MessageCheckRemove.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'MessageCheckRemove'
|
||||
|
||||
export default Icon
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M15.2 3H7.8C6.11984 3 5.27976 3 4.63803 3.32698C4.07354 3.6146 3.6146 4.07354 3.32698 4.63803C3 5.27976 3 6.11984 3 7.8V14C3 14.93 3 15.395 3.10222 15.7765C3.37962 16.8117 4.18827 17.6204 5.22354 17.8978C5.60504 18 6.07003 18 7 18V20.3355C7 20.8684 7 21.1348 7.10923 21.2716C7.20422 21.3906 7.34827 21.4599 7.50054 21.4597C7.67563 21.4595 7.88367 21.2931 8.29976 20.9602L10.6852 19.0518C11.1725 18.662 11.4162 18.4671 11.6875 18.3285C11.9282 18.2055 12.1844 18.1156 12.4492 18.0613C12.7477 18 13.0597 18 13.6837 18H16.2C17.8802 18 18.7202 18 19.362 17.673C19.9265 17.3854 20.3854 16.9265 20.673 16.362C21 15.7202 21 14.8802 21 13.2V8.8M12.3333 13.5L14 10.5H10L11.6667 7.5M21 5V3M21 3V1M21 3H19M21 3H23",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "MessageFastPlus"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './MessageFastPlus.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'MessageFastPlus'
|
||||
|
||||
export default Icon
|
|
@ -1 +1,3 @@
|
|||
export { default as ChatBot } from './ChatBot'
|
||||
export { default as MessageCheckRemove } from './MessageCheckRemove'
|
||||
export { default as MessageFastPlus } from './MessageFastPlus'
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M20 12.5V6.8C20 5.11984 20 4.27976 19.673 3.63803C19.3854 3.07354 18.9265 2.6146 18.362 2.32698C17.7202 2 16.8802 2 15.2 2H8.8C7.11984 2 6.27976 2 5.63803 2.32698C5.07354 2.6146 4.6146 3.07354 4.32698 3.63803C4 4.27976 4 5.11984 4 6.8V17.2C4 18.8802 4 19.7202 4.32698 20.362C4.6146 20.9265 5.07354 21.3854 5.63803 21.673C6.27976 22 7.1198 22 8.79986 22H12.5M14 11H8M10 15H8M16 7H8M15 19L18 22M18 22L21 19M18 22V16",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "FileDownload02"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './FileDownload02.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'FileDownload02'
|
||||
|
||||
export default Icon
|
|
@ -1,4 +1,5 @@
|
|||
export { default as ClipboardCheck } from './ClipboardCheck'
|
||||
export { default as Clipboard } from './Clipboard'
|
||||
export { default as File02 } from './File02'
|
||||
export { default as FileDownload02 } from './FileDownload02'
|
||||
export { default as FilePlus02 } from './FilePlus02'
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M21 18L19.9999 19.094C19.4695 19.6741 18.7502 20 18.0002 20C17.2501 20 16.5308 19.6741 16.0004 19.094C15.4693 18.5151 14.75 18.1901 14.0002 18.1901C13.2504 18.1901 12.5312 18.5151 12 19.094M3.00003 20H4.67457C5.16376 20 5.40835 20 5.63852 19.9447C5.84259 19.8957 6.03768 19.8149 6.21663 19.7053C6.41846 19.5816 6.59141 19.4086 6.93732 19.0627L19.5001 6.49998C20.3285 5.67156 20.3285 4.32841 19.5001 3.49998C18.6716 2.67156 17.3285 2.67156 16.5001 3.49998L3.93729 16.0627C3.59139 16.4086 3.41843 16.5816 3.29475 16.7834C3.18509 16.9624 3.10428 17.1574 3.05529 17.3615C3.00003 17.5917 3.00003 17.8363 3.00003 18.3255V20Z",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Edit04"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Edit04.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Edit04'
|
||||
|
||||
export default Icon
|
|
@ -4,6 +4,7 @@ export { default as Check } from './Check'
|
|||
export { default as DotsHorizontal } from './DotsHorizontal'
|
||||
export { default as Edit02 } from './Edit02'
|
||||
export { default as Edit03 } from './Edit03'
|
||||
export { default as Edit04 } from './Edit04'
|
||||
export { default as Hash02 } from './Hash02'
|
||||
export { default as HelpCircle } from './HelpCircle'
|
||||
export { default as InfoCircle } from './InfoCircle'
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"d": "M22.7 11.5L20.7005 13.5L18.7 11.5M20.9451 13C20.9814 12.6717 21 12.338 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C14.8273 21 17.35 19.6963 19 17.6573M12 7V12L15 14",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "2",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "ClockFastForward"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './ClockFastForward.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'ClockFastForward'
|
||||
|
||||
export default Icon
|
|
@ -0,0 +1 @@
|
|||
export { default as ClockFastForward } from './ClockFastForward'
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M16.2414 2H7.7588C6.95383 1.99999 6.28946 1.99998 5.74827 2.04419C5.18617 2.09012 4.66947 2.18868 4.18413 2.43598C3.43149 2.81947 2.81956 3.43139 2.43607 4.18404C2.18878 4.66937 2.09022 5.18608 2.04429 5.74818C2.00007 6.28937 2.00008 6.95373 2.0001 7.7587L2.00005 14.1376C1.99962 14.933 1.9993 15.5236 2.13639 16.0353C2.50626 17.4156 3.58445 18.4938 4.96482 18.8637C5.27229 18.9461 5.60829 18.9789 6.0001 18.9918L6.00009 20.371C6.00005 20.6062 6 20.846 6.01785 21.0425C6.03492 21.2305 6.08012 21.5852 6.32778 21.8955C6.61276 22.2525 7.0449 22.4602 7.50172 22.4597C7.8987 22.4593 8.20394 22.273 8.36137 22.1689C8.52597 22.06 8.7132 21.9102 8.89688 21.7632L11.31 19.8327C11.8286 19.4178 11.9826 19.3007 12.1425 19.219C12.303 19.137 12.4738 19.0771 12.6504 19.0408C12.8263 19.0047 13.0197 19 13.6838 19H16.2414C17.0464 19 17.7107 19 18.2519 18.9558C18.814 18.9099 19.3307 18.8113 19.8161 18.564C20.5687 18.1805 21.1806 17.5686 21.5641 16.816C21.8114 16.3306 21.91 15.8139 21.9559 15.2518C22.0001 14.7106 22.0001 14.0463 22.0001 13.2413V7.75868C22.0001 6.95372 22.0001 6.28936 21.9559 5.74818C21.91 5.18608 21.8114 4.66937 21.5641 4.18404C21.1806 3.43139 20.5687 2.81947 19.8161 2.43598C19.3307 2.18868 18.814 2.09012 18.2519 2.04419C17.7107 1.99998 17.0464 1.99999 16.2414 2ZM12.681 5.5349C12.8938 5.61898 13.0218 5.83714 12.9916 6.06386L12.5688 9.23501L14.48 9.23501C14.5899 9.23498 14.7038 9.23496 14.7979 9.24356C14.8905 9.25203 15.0589 9.27446 15.2095 9.39066C15.3851 9.52617 15.4913 9.73269 15.4996 9.95432C15.5066 10.1444 15.427 10.2945 15.38 10.3747C15.3324 10.4563 15.2661 10.549 15.2022 10.6384L11.9072 15.2514C11.7743 15.4375 11.5317 15.5092 11.319 15.4251C11.1063 15.341 10.9782 15.1229 11.0084 14.8961L11.4312 11.725L9.52004 11.725C9.41011 11.725 9.29618 11.725 9.20206 11.7164C9.10948 11.708 8.94106 11.6855 8.79051 11.5693C8.61493 11.4338 8.50866 11.2273 8.50044 11.0057C8.49339 10.8156 8.57303 10.6655 8.61996 10.5853C8.66766 10.5037 8.7339 10.411 8.79781 10.3216L12.0928 5.70858C12.2257 5.52246 12.4683 5.45083 12.681 5.5349Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "MessageFast"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './MessageFast.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'MessageFast'
|
||||
|
||||
export default Icon
|
|
@ -0,0 +1 @@
|
|||
export { default as MessageFast } from './MessageFast'
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"icon": {
|
||||
"type": "element",
|
||||
"isRootNode": true,
|
||||
"name": "svg",
|
||||
"attributes": {
|
||||
"width": "24",
|
||||
"height": "24",
|
||||
"viewBox": "0 0 24 24",
|
||||
"fill": "none",
|
||||
"xmlns": "http://www.w3.org/2000/svg"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M21.6747 17.2619C22.0824 17.6345 22.1107 18.2671 21.7381 18.6747L20.738 19.7687C20.0284 20.5448 19.0458 21 18.0002 21C16.9549 21 15.9726 20.5452 15.2631 19.7696C14.9112 19.3863 14.4549 19.1901 14.0002 19.1901C13.5454 19.1901 13.0889 19.3864 12.7369 19.7701C12.3635 20.177 11.7309 20.2043 11.324 19.8309C10.917 19.4575 10.8898 18.8249 11.2632 18.418C11.9735 17.6438 12.9555 17.1901 14.0002 17.1901C15.045 17.1901 16.0269 17.6438 16.7373 18.418L16.7384 18.4192C17.0897 18.8034 17.5458 19 18.0002 19C18.4545 19 18.9106 18.8034 19.2618 18.4193L20.2619 17.3253C20.6346 16.9177 21.2671 16.8893 21.6747 17.2619Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M15.793 2.79287C17.0119 1.57393 18.9882 1.57392 20.2072 2.79287C21.4261 4.01183 21.4261 5.98814 20.2072 7.20709L7.64443 19.7698C7.62463 19.7896 7.60502 19.8093 7.58556 19.8288C7.29811 20.1168 7.04467 20.3707 6.73914 20.5579C6.47072 20.7224 6.17809 20.8436 5.87198 20.9171C5.52353 21.0007 5.16478 21.0004 4.75788 21C4.73034 21 4.70258 21 4.67458 21H3.00004C2.44776 21 2.00004 20.5523 2.00004 20V18.3255C2.00004 18.2975 2.00001 18.2697 1.99999 18.2422C1.99961 17.8353 1.99928 17.4765 2.08293 17.1281C2.15642 16.822 2.27763 16.5293 2.44212 16.2609C2.62936 15.9554 2.88327 15.7019 3.17125 15.4145C3.19075 15.395 3.2104 15.3754 3.23019 15.3556L15.793 2.79287Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Edit04"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Edit04.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Edit04'
|
||||
|
||||
export default Icon
|
|
@ -1,6 +1,7 @@
|
|||
export { default as CheckCircle } from './CheckCircle'
|
||||
export { default as CheckDone01 } from './CheckDone01'
|
||||
export { default as Download02 } from './Download02'
|
||||
export { default as Edit04 } from './Edit04'
|
||||
export { default as Eye } from './Eye'
|
||||
export { default as MessageClockCircle } from './MessageClockCircle'
|
||||
export { default as Target04 } from './Target04'
|
||||
|
|
|
@ -81,11 +81,11 @@ const useLazyLoad = (ref: RefObject<Element>): boolean => {
|
|||
return isIntersecting
|
||||
}
|
||||
|
||||
export function Markdown(props: { content: string }) {
|
||||
export function Markdown(props: { content: string; className?: string }) {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
const [isSVG, setIsSVG] = useState(false)
|
||||
return (
|
||||
<div className="markdown-body">
|
||||
<div className={cn(props.className, 'markdown-body')}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
|
||||
rehypePlugins={[
|
||||
|
@ -120,7 +120,7 @@ export function Markdown(props: { content: string }) {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
{ (language === 'mermaid' && isSVG)
|
||||
{(language === 'mermaid' && isSVG)
|
||||
? (<Flowchart PrimitiveCode={String(children).replace(/\n$/, '')} />)
|
||||
: (<SyntaxHighlighter
|
||||
{...props}
|
||||
|
|
66
web/app/components/base/modal/delete-confirm-modal/index.tsx
Normal file
|
@ -0,0 +1,66 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import s from './style.module.css'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { AlertCircle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
|
||||
|
||||
type Props = {
|
||||
isShow: boolean
|
||||
onHide: () => void
|
||||
onRemove: () => void
|
||||
text?: string
|
||||
children?: JSX.Element
|
||||
}
|
||||
|
||||
const DeleteConfirmModal: FC<Props> = ({
|
||||
isShow,
|
||||
onHide,
|
||||
onRemove,
|
||||
children,
|
||||
text,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
if (!isShow)
|
||||
return null
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={isShow}
|
||||
onClose={onHide}
|
||||
wrapperClassName='z-50'
|
||||
className={cn(s.delModal, 'z-50')}
|
||||
closable
|
||||
>
|
||||
<div onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
}}>
|
||||
<div className={s.warningWrapper}>
|
||||
<AlertCircle className='w-6 h-6 text-red-600' />
|
||||
</div>
|
||||
{text
|
||||
? (
|
||||
<div className='text-xl font-semibold text-gray-900 mb-3'>{text}</div>
|
||||
)
|
||||
: children}
|
||||
|
||||
<div className='flex gap-2 justify-end'>
|
||||
<Button onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
<Button
|
||||
type='warning'
|
||||
onClick={onRemove}
|
||||
className='border-red-700 border-[0.5px]'
|
||||
>
|
||||
{t('common.operation.sure')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(DeleteConfirmModal)
|
|
@ -0,0 +1,16 @@
|
|||
.delModal {
|
||||
background: linear-gradient(180deg,
|
||||
rgba(217, 45, 32, 0.05) 0%,
|
||||
rgba(217, 45, 32, 0) 24.02%),
|
||||
#f9fafb;
|
||||
box-shadow: 0px 20px 24px -4px rgba(16, 24, 40, 0.08),
|
||||
0px 8px 8px -4px rgba(16, 24, 40, 0.03);
|
||||
@apply rounded-2xl p-8;
|
||||
}
|
||||
|
||||
.warningWrapper {
|
||||
box-shadow: 0px 20px 24px -4px rgba(16, 24, 40, 0.08),
|
||||
0px 8px 8px -4px rgba(16, 24, 40, 0.03);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
@apply h-12 w-12 border-[0.5px] border-gray-100 rounded-xl mb-3 flex items-center justify-center;
|
||||
}
|
|
@ -63,8 +63,13 @@ export default function Modal({
|
|||
{description}
|
||||
</Dialog.Description>}
|
||||
{closable
|
||||
&& <div className='absolute top-6 right-6 w-5 h-5 rounded-2xl flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
|
||||
<XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
|
||||
&& <div className='absolute z-10 top-6 right-6 w-5 h-5 rounded-2xl flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
|
||||
<XMarkIcon className='w-4 h-4 text-gray-500' onClick={
|
||||
(e) => {
|
||||
e.stopPropagation()
|
||||
onClose()
|
||||
}
|
||||
} />
|
||||
</div>}
|
||||
{children}
|
||||
</Dialog.Panel>
|
||||
|
|
68
web/app/components/base/tab-slider-plain/index.tsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import cn from 'classnames'
|
||||
|
||||
type Option = {
|
||||
value: string
|
||||
text: string | JSX.Element
|
||||
}
|
||||
|
||||
type ItemProps = {
|
||||
className?: string
|
||||
isActive: boolean
|
||||
onClick: (v: string) => void
|
||||
option: Option
|
||||
}
|
||||
const Item: FC<ItemProps> = ({
|
||||
className,
|
||||
isActive,
|
||||
onClick,
|
||||
option,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
key={option.value}
|
||||
className={cn(className, !isActive && 'cursor-pointer', 'relative pb-2.5 leading-6 text-base font-semibold')}
|
||||
onClick={() => !isActive && onClick(option.value)}
|
||||
>
|
||||
<div className={cn(isActive ? 'text-gray-900' : 'text-gray-600')}>{option.text}</div>
|
||||
{isActive && (
|
||||
<div className='absolute bottom-0 left-0 right-0 h-0.5 bg-[#155EEF]'></div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
value: string
|
||||
onChange: (v: string) => void
|
||||
options: Option[]
|
||||
noBorderBottom?: boolean
|
||||
itemClassName?: string
|
||||
}
|
||||
|
||||
const TabSlider: FC<Props> = ({
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
noBorderBottom,
|
||||
itemClassName,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn(className, !noBorderBottom && 'border-b border-[#EAECF0]', 'flex space-x-6')}>
|
||||
{options.map(option => (
|
||||
<Item
|
||||
isActive={option.value === value}
|
||||
option={option}
|
||||
onClick={onChange}
|
||||
key={option.value}
|
||||
className={itemClassName}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(TabSlider)
|
31
web/app/components/billing/annotation-full/index.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import UpgradeBtn from '../upgrade-btn'
|
||||
import Usage from './usage'
|
||||
import s from './style.module.css'
|
||||
import GridMask from '@/app/components/base/grid-mask'
|
||||
|
||||
const AnnotationFull: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<GridMask wrapperClassName='rounded-lg' canvasClassName='rounded-lg' gradientClassName='rounded-lg'>
|
||||
<div className='mt-6 px-3.5 py-4 border-2 border-solid border-transparent rounded-lg shadow-md flex flex-col transition-all duration-200 ease-in-out cursor-pointer'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className={cn(s.textGradient, 'leading-[24px] text-base font-semibold')}>
|
||||
<div>{t('billing.annotatedResponse.fullTipLine1')}</div>
|
||||
<div>{t('billing.annotatedResponse.fullTipLine2')}</div>
|
||||
</div>
|
||||
<div className='flex'>
|
||||
<UpgradeBtn loc={'annotation-create'} />
|
||||
</div>
|
||||
</div>
|
||||
<Usage className='mt-4' />
|
||||
</div>
|
||||
</GridMask>
|
||||
)
|
||||
}
|
||||
export default React.memo(AnnotationFull)
|
47
web/app/components/billing/annotation-full/modal.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import UpgradeBtn from '../upgrade-btn'
|
||||
import Modal from '../../base/modal'
|
||||
import Usage from './usage'
|
||||
import s from './style.module.css'
|
||||
import GridMask from '@/app/components/base/grid-mask'
|
||||
|
||||
type Props = {
|
||||
show: boolean
|
||||
onHide: () => void
|
||||
}
|
||||
const AnnotationFullModal: FC<Props> = ({
|
||||
show,
|
||||
onHide,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow={show}
|
||||
onClose={onHide}
|
||||
closable
|
||||
className='!p-0'
|
||||
>
|
||||
<GridMask wrapperClassName='rounded-lg' canvasClassName='rounded-lg' gradientClassName='rounded-lg'>
|
||||
<div className='mt-6 px-7 py-6 border-2 border-solid border-transparent rounded-lg shadow-md flex flex-col transition-all duration-200 ease-in-out cursor-pointer'>
|
||||
<div className='flex justify-between items-center'>
|
||||
<div className={cn(s.textGradient, 'leading-[27px] text-[18px] font-semibold')}>
|
||||
<div>{t('billing.annotatedResponse.fullTipLine1')}</div>
|
||||
<div>{t('billing.annotatedResponse.fullTipLine2')}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<Usage className='mt-4' />
|
||||
<div className='mt-7 flex justify-end'>
|
||||
<UpgradeBtn loc={'annotation-create'} />
|
||||
</div>
|
||||
</div>
|
||||
</GridMask>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(AnnotationFullModal)
|
|
@ -0,0 +1,7 @@
|
|||
.textGradient {
|
||||
background: linear-gradient(92deg, #2250F2 -29.55%, #0EBCF3 75.22%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-fill-color: transparent;
|
||||
}
|
32
web/app/components/billing/annotation-full/usage.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { MessageFastPlus } from '../../base/icons/src/vender/line/communication'
|
||||
import UsageInfo from '../usage-info'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Usage: FC<Props> = ({
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { plan } = useProviderContext()
|
||||
const {
|
||||
usage,
|
||||
total,
|
||||
} = plan
|
||||
return (
|
||||
<UsageInfo
|
||||
className={className}
|
||||
Icon={MessageFastPlus}
|
||||
name={t('billing.annotatedResponse.quotaTitle')}
|
||||
usage={usage.annotatedResponse}
|
||||
total={total.annotatedResponse}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default React.memo(Usage)
|
|
@ -63,10 +63,12 @@ export const defaultPlan = {
|
|||
vectorSpace: 1,
|
||||
buildApps: 1,
|
||||
teamMembers: 1,
|
||||
annotatedResponse: 1,
|
||||
},
|
||||
total: {
|
||||
vectorSpace: 10,
|
||||
buildApps: 10,
|
||||
teamMembers: 1,
|
||||
annotatedResponse: 10,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ const ProgressBar = ({
|
|||
color = '#2970FF',
|
||||
}: ProgressBarProps) => {
|
||||
return (
|
||||
<div className='bg-[#F2F4F7] rounded-[4px]'>
|
||||
<div className='bg-[#F2F4F7] rounded-[4px] overflow-hidden'>
|
||||
<div
|
||||
className='h-2 rounded-[4px]'
|
||||
style={{
|
||||
|
|
|
@ -23,7 +23,7 @@ export type PlanInfo = {
|
|||
annotatedResponse: number
|
||||
}
|
||||
|
||||
export type UsagePlanInfo = Pick<PlanInfo, 'vectorSpace' | 'buildApps' | 'teamMembers'>
|
||||
export type UsagePlanInfo = Pick<PlanInfo, 'vectorSpace' | 'buildApps' | 'teamMembers' | 'annotatedResponse'>
|
||||
|
||||
export enum DocumentProcessingPriority {
|
||||
standard = 'standard',
|
||||
|
@ -48,6 +48,10 @@ export type CurrentPlanInfoBackend = {
|
|||
size: number
|
||||
limit: number // total. 0 means unlimited
|
||||
}
|
||||
annotation_quota_limit: {
|
||||
size: number
|
||||
limit: number // total. 0 means unlimited
|
||||
}
|
||||
docs_processing: DocumentProcessingPriority
|
||||
}
|
||||
|
||||
|
|
|
@ -15,11 +15,13 @@ export const parseCurrentPlan = (data: CurrentPlanInfoBackend) => {
|
|||
vectorSpace: data.vector_space.size,
|
||||
buildApps: data.apps?.size || 0,
|
||||
teamMembers: data.members.size,
|
||||
annotatedResponse: data.annotation_quota_limit.size,
|
||||
},
|
||||
total: {
|
||||
vectorSpace: parseLimit(data.vector_space.limit),
|
||||
buildApps: parseLimit(data.apps?.limit) || 0,
|
||||
teamMembers: parseLimit(data.members.limit),
|
||||
annotatedResponse: parseLimit(data.annotation_quota_limit.limit),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,358 @@
|
|||
import type { FC } from 'react'
|
||||
import React, { Fragment, useEffect, useRef, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import _ from 'lodash-es'
|
||||
import cn from 'classnames'
|
||||
import ModelModal from '../model-modal'
|
||||
import cohereConfig from '../configs/cohere'
|
||||
import s from './style.module.css'
|
||||
import type { BackendModel, FormValue, ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations'
|
||||
import { ModelType } from '@/app/components/header/account-setting/model-page/declarations'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import { Check, LinkExternal01, SearchLg } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { AlertCircle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import ModelIcon from '@/app/components/app/configuration/config-model/model-icon'
|
||||
import ModelName from '@/app/components/app/configuration/config-model/model-name'
|
||||
import ProviderName from '@/app/components/app/configuration/config-model/provider-name'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import ModelModeTypeLabel from '@/app/components/app/configuration/config-model/model-mode-type-label'
|
||||
import type { ModelModeType } from '@/types/app'
|
||||
import { CubeOutline } from '@/app/components/base/icons/src/vender/line/shapes'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { fetchDefaultModal, setModelProvider } from '@/service/common'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
|
||||
type Props = {
|
||||
value: {
|
||||
providerName: ProviderEnum
|
||||
modelName: string
|
||||
} | undefined
|
||||
modelType: ModelType
|
||||
isShowModelModeType?: boolean
|
||||
isShowAddModel?: boolean
|
||||
supportAgentThought?: boolean
|
||||
onChange: (value: BackendModel) => void
|
||||
popClassName?: string
|
||||
readonly?: boolean
|
||||
triggerIconSmall?: boolean
|
||||
whenEmptyGoToSetting?: boolean
|
||||
onUpdate?: () => void
|
||||
widthSameToTrigger?: boolean
|
||||
}
|
||||
|
||||
type ModelOption = {
|
||||
type: 'model'
|
||||
value: string
|
||||
providerName: ProviderEnum
|
||||
modelDisplayName: string
|
||||
model_mode: ModelModeType
|
||||
} | {
|
||||
type: 'provider'
|
||||
value: ProviderEnum
|
||||
}
|
||||
|
||||
const ModelSelector: FC<Props> = ({
|
||||
value,
|
||||
modelType,
|
||||
isShowModelModeType,
|
||||
isShowAddModel,
|
||||
supportAgentThought,
|
||||
onChange,
|
||||
popClassName,
|
||||
readonly,
|
||||
triggerIconSmall,
|
||||
whenEmptyGoToSetting,
|
||||
onUpdate,
|
||||
widthSameToTrigger,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
const {
|
||||
textGenerationModelList,
|
||||
embeddingsModelList,
|
||||
speech2textModelList,
|
||||
rerankModelList,
|
||||
agentThoughtModelList,
|
||||
updateModelList,
|
||||
} = useProviderContext()
|
||||
const [search, setSearch] = useState('')
|
||||
const modelList = supportAgentThought
|
||||
? agentThoughtModelList
|
||||
: ({
|
||||
[ModelType.textGeneration]: textGenerationModelList,
|
||||
[ModelType.embeddings]: embeddingsModelList,
|
||||
[ModelType.speech2text]: speech2textModelList,
|
||||
[ModelType.reranking]: rerankModelList,
|
||||
})[modelType]
|
||||
const currModel = modelList.find(item => item.model_name === value?.modelName && item.model_provider.provider_name === value.providerName)
|
||||
const allModelNames = (() => {
|
||||
if (!search)
|
||||
return {}
|
||||
|
||||
const res: Record<string, string> = {}
|
||||
modelList.forEach(({ model_name, model_display_name }) => {
|
||||
res[model_name] = model_display_name
|
||||
})
|
||||
return res
|
||||
})()
|
||||
const filteredModelList = search
|
||||
? modelList.filter(({ model_name }) => {
|
||||
if (allModelNames[model_name].includes(search))
|
||||
return true
|
||||
|
||||
return false
|
||||
})
|
||||
: modelList
|
||||
|
||||
const hasRemoved = (value && value.modelName && value.providerName) && !modelList.find(({ model_name, model_provider }) => model_name === value.modelName && model_provider.provider_name === value.providerName)
|
||||
|
||||
const modelOptions: ModelOption[] = (() => {
|
||||
const providers = _.uniq(filteredModelList.map(item => item.model_provider.provider_name))
|
||||
const res: ModelOption[] = []
|
||||
providers.forEach((providerName) => {
|
||||
res.push({
|
||||
type: 'provider',
|
||||
value: providerName,
|
||||
})
|
||||
const models = filteredModelList.filter(m => m.model_provider.provider_name === providerName)
|
||||
models.forEach(({ model_name, model_display_name, model_mode }) => {
|
||||
res.push({
|
||||
type: 'model',
|
||||
providerName,
|
||||
value: model_name,
|
||||
modelDisplayName: model_display_name,
|
||||
model_mode,
|
||||
})
|
||||
})
|
||||
})
|
||||
return res
|
||||
})()
|
||||
const { eventEmitter } = useEventEmitterContextContext()
|
||||
const [showRerankModal, setShowRerankModal] = useState(false)
|
||||
const [shouldFetchRerankDefaultModel, setShouldFetchRerankDefaultModel] = useState(false)
|
||||
const { notify } = useToastContext()
|
||||
const { data: rerankDefaultModel } = useSWR(shouldFetchRerankDefaultModel ? '/workspaces/current/default-model?model_type=reranking' : null, fetchDefaultModal)
|
||||
const handleOpenRerankModal = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation()
|
||||
setShowRerankModal(true)
|
||||
}
|
||||
const handleRerankModalSave = async (originValue?: FormValue) => {
|
||||
if (originValue) {
|
||||
try {
|
||||
eventEmitter?.emit('provider-save')
|
||||
const res = await setModelProvider({
|
||||
url: `/workspaces/current/model-providers/${cohereConfig.modal.key}`,
|
||||
body: {
|
||||
config: originValue,
|
||||
},
|
||||
})
|
||||
if (res.result === 'success') {
|
||||
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
updateModelList(ModelType.reranking)
|
||||
setShowRerankModal(false)
|
||||
setShouldFetchRerankDefaultModel(true)
|
||||
if (onUpdate)
|
||||
onUpdate()
|
||||
}
|
||||
eventEmitter?.emit('')
|
||||
}
|
||||
catch (e) {
|
||||
eventEmitter?.emit('')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const triggerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (rerankDefaultModel && whenEmptyGoToSetting)
|
||||
onChange(rerankDefaultModel)
|
||||
}, [rerankDefaultModel])
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
offset={4}
|
||||
>
|
||||
<div className='relative'>
|
||||
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)} className={cn('flex items-center px-2.5 w-full h-9 rounded-lg', readonly ? '!cursor-auto bg-gray-100 opacity-50' : 'bg-gray-100', hasRemoved && '!bg-[#FEF3F2]')}>
|
||||
{
|
||||
<div ref={triggerRef} className='flex items-center w-full cursor-pointer'>
|
||||
{
|
||||
(value && value.modelName && value.providerName)
|
||||
? (
|
||||
<>
|
||||
<ModelIcon
|
||||
className={cn('mr-1.5', !triggerIconSmall && 'w-5 h-5')}
|
||||
modelId={value.modelName}
|
||||
providerName={value.providerName}
|
||||
/>
|
||||
<div className='mr-1.5 grow flex items-center text-left text-sm text-gray-900 truncate'>
|
||||
<ModelName modelId={value.modelName} modelDisplayName={currModel?.model_display_name || value.modelName} />
|
||||
{isShowModelModeType && (
|
||||
<ModelModeTypeLabel className='ml-2' type={currModel?.model_mode as ModelModeType} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
: whenEmptyGoToSetting
|
||||
? (
|
||||
<div className='grow flex items-center h-9 justify-between' onClick={handleOpenRerankModal}>
|
||||
<div className='flex items-center text-[13px] font-medium text-primary-500'>
|
||||
<CubeOutline className='mr-1.5 w-4 h-4' />
|
||||
{t('common.modelProvider.selector.rerankTip')}
|
||||
</div>
|
||||
<LinkExternal01 className='w-3 h-3 text-gray-500' />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className='grow text-left text-sm text-gray-800 opacity-60'>{t('common.modelProvider.selectModel')}</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
hasRemoved && (
|
||||
<Tooltip
|
||||
selector='model-selector-remove-tip'
|
||||
htmlContent={
|
||||
<div className='w-[261px] text-gray-500'>{t('common.modelProvider.selector.tip')}</div>
|
||||
}
|
||||
>
|
||||
<AlertCircle className='mr-1 w-4 h-4 text-[#F04438]' />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
{
|
||||
!readonly && !whenEmptyGoToSetting && (
|
||||
<ChevronDown className={`w-4 h-4 text-gray-700 ${open ? 'opacity-100' : 'opacity-60'}`} />
|
||||
)
|
||||
}
|
||||
{
|
||||
whenEmptyGoToSetting && (value && value.modelName && value.providerName) && (
|
||||
<ChevronDown className={`w-4 h-4 text-gray-700 ${open ? 'opacity-100' : 'opacity-60'}`} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</PortalToFollowElemTrigger>
|
||||
{!readonly && (
|
||||
<PortalToFollowElemContent
|
||||
className={cn(popClassName, !widthSameToTrigger && (isShowModelModeType ? 'max-w-[312px]' : 'max-w-[260px]'), 'absolute top-10 p-1 min-w-[232px] max-h-[366px] bg-white border-[0.5px] border-gray-200 rounded-lg shadow-lg overflow-auto z-[999]')}
|
||||
style={{
|
||||
width: (widthSameToTrigger && triggerRef.current?.offsetWidth) ? `${triggerRef.current?.offsetWidth}px` : 'auto',
|
||||
}}
|
||||
>
|
||||
<div className='px-2 pt-2 pb-1'>
|
||||
<div className='flex items-center px-2 h-8 bg-gray-100 rounded-lg'>
|
||||
<div className='mr-1.5 p-[1px]'><SearchLg className='w-[14px] h-[14px] text-gray-400' /></div>
|
||||
<div className='grow px-0.5'>
|
||||
<input
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
className={`
|
||||
block w-full h-8 bg-transparent text-[13px] text-gray-700
|
||||
outline-none appearance-none border-none
|
||||
`}
|
||||
placeholder={t('common.modelProvider.searchModel') || ''}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
search && (
|
||||
<div className='ml-1 p-0.5 cursor-pointer' onClick={() => setSearch('')}>
|
||||
<XCircle className='w-3 h-3 text-gray-400' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
modelOptions.map((model) => {
|
||||
if (model.type === 'provider') {
|
||||
return (
|
||||
<div
|
||||
className='px-3 pt-2 pb-1 text-xs font-medium text-gray-500'
|
||||
key={`${model.type}-${model.value}`}
|
||||
>
|
||||
<ProviderName provideName={model.value} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (model.type === 'model') {
|
||||
return (
|
||||
<div
|
||||
key={`${model.providerName}-${model.value}`}
|
||||
className={`${s.optionItem}
|
||||
flex items-center px-3 w-full h-8 rounded-lg hover:bg-gray-50
|
||||
${!readonly ? 'cursor-pointer' : 'cursor-auto'}
|
||||
${(value?.providerName === model.providerName && value?.modelName === model.value) && 'bg-gray-50'}
|
||||
`}
|
||||
onClick={() => {
|
||||
const selectedModel = modelList.find((item) => {
|
||||
return item.model_name === model.value && item.model_provider.provider_name === model.providerName
|
||||
})
|
||||
onChange(selectedModel as BackendModel)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<ModelIcon
|
||||
className='mr-2 shrink-0'
|
||||
modelId={model.value}
|
||||
providerName={model.providerName}
|
||||
/>
|
||||
<div className='mr-2 grow flex items-center text-left text-sm text-gray-900 truncate'>
|
||||
<ModelName modelId={model.value} modelDisplayName={model.modelDisplayName} />
|
||||
{isShowModelModeType && (
|
||||
<ModelModeTypeLabel className={`${s.modelModeLabel} ml-2`} type={model.model_mode} />
|
||||
)}
|
||||
</div>
|
||||
{(value?.providerName === model.providerName && value?.modelName === model.value) && <Check className='shrink-0 w-4 h-4 text-primary-600' />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
}
|
||||
{modelList.length !== 0 && (search && filteredModelList.length === 0) && (
|
||||
<div className='px-3 pt-1.5 h-[30px] text-center text-xs text-gray-500'>{t('common.modelProvider.noModelFound', { model: search })}</div>
|
||||
)}
|
||||
|
||||
{isShowAddModel && (
|
||||
<div
|
||||
className='border-t flex items-center h-9 pl-3 text-xs text-[#155EEF] cursor-pointer'
|
||||
style={{
|
||||
borderColor: 'rgba(0, 0, 0, 0.05)',
|
||||
}}
|
||||
onClick={() => setShowAccountSettingModal({ payload: 'provider' })}
|
||||
>
|
||||
<CubeOutline className='w-4 h-4 mr-2' />
|
||||
<div>{t('common.model.addMoreModel')}</div>
|
||||
</div>
|
||||
)}
|
||||
</PortalToFollowElemContent>
|
||||
)}
|
||||
</div>
|
||||
<ModelModal
|
||||
isShow={showRerankModal}
|
||||
modelModal={cohereConfig.modal}
|
||||
onCancel={() => setShowRerankModal(false)}
|
||||
onSave={handleRerankModalSave}
|
||||
mode={'add'}
|
||||
/>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelSelector
|
|
@ -613,6 +613,22 @@ const Main: FC<IMainProps> = ({
|
|||
))
|
||||
}
|
||||
},
|
||||
onAnnotationReply: (annotationReply) => {
|
||||
responseItem.content = annotationReply.answer
|
||||
const newListWithAnswer = produce(
|
||||
getChatList().filter(item => item.id !== responseItem.id && item.id !== placeholderAnswerId),
|
||||
(draft) => {
|
||||
if (!draft.find(item => item.id === questionId))
|
||||
draft.push({ ...questionItem })
|
||||
|
||||
draft.push({
|
||||
...responseItem,
|
||||
id: annotationReply.id,
|
||||
})
|
||||
})
|
||||
setChatList(newListWithAnswer)
|
||||
tempNewConversationId = annotationReply.conversation_id
|
||||
},
|
||||
onError() {
|
||||
setResponsingFalse()
|
||||
// role back placeholder answer
|
||||
|
|
|
@ -14,6 +14,7 @@ import type { PromptConfig } from '@/models/debug'
|
|||
import type { InstalledApp } from '@/models/explore'
|
||||
import type { ModerationService } from '@/models/common'
|
||||
import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app'
|
||||
|
||||
export type IResultProps = {
|
||||
isCallBatchAPI: boolean
|
||||
isPC: boolean
|
||||
|
|