feat: annotation management frontend (#1764)

This commit is contained in:
Joel 2023-12-18 15:41:24 +08:00 committed by GitHub
parent 96d2de2258
commit 65fd4b39ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
122 changed files with 4718 additions and 214 deletions

View File

@ -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

View File

@ -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} />
)
}

View File

@ -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 (

View File

@ -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)

View 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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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')}&nbsp;{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)

View 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)

View 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)

View 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)

View File

@ -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;
}

View 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)

View 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)

View File

@ -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)

View File

@ -0,0 +1,9 @@
.logTable td {
padding: 7px 8px;
box-sizing: border-box;
max-width: 200px;
}
.pagination li {
list-style: none;
}

View 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',
}

View File

@ -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)

View File

@ -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')}&nbsp;{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)

View File

@ -0,0 +1,9 @@
.table td {
padding: 7px 8px;
box-sizing: border-box;
max-width: 200px;
}
.pagination li {
list-style: none;
}

View File

@ -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>
)}
{!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>
<Markdown content={annotation?.logAnnotation ? annotation?.logAnnotation.content : content} />
</div>
</>
}
{(hasAnnotation && !annotation?.logAnnotation) && (
<EditTitle className='mt-1' title={t('appAnnotation.editBy', {
author: annotation.authorName,
})} />
)}
</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>

View File

@ -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 (

View File

@ -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;
}

View File

@ -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
}

View File

@ -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>
)
}

View File

@ -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)
}

View File

@ -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>
</>
)

View File

@ -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')} />

View File

@ -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} />)}
</>
)

View File

@ -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

View File

@ -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>
)}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1,4 @@
export enum PageType {
log = 'log',
annotation = 'annotation',
}

View File

@ -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

View File

@ -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>
)
}

View File

@ -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

View File

@ -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;
}

View File

@ -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)

View 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)

View File

@ -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'>
<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>

View File

@ -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'}

View File

@ -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,99 +180,8 @@ const GenerationItem: FC<IGenerationItemProps> = ({
setModal(true)
}
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
? {
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
}
: {}}
>
{isLoading
? (
<div className='flex items-center h-10'><Loading type='area' /></div>
)
: (
<div
className={cn(!isTop && 'rounded-br-xl border-l-2 border-primary-400', 'p-4')}
style={mainStyle}
>
{(isTop && taskId) && (
<div className='mb-2 text-gray-500 border border-gray-200 box-border flex items-center rounded-md italic text-[11px] pl-1 pr-1.5 font-medium w-fit group-hover:opacity-100'>
<HashtagIcon className='w-3 h-3 text-gray-400 fill-current mr-1 stroke-current stroke-1' />
{taskId}
</div>)
}
<div className='flex'>
<div className='grow w-0'>
{isError
? <div className='text-gray-400 text-sm'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
: (
<Markdown content={ content } />
)}
</div>
</div>
<div className='flex items-center justify-between mt-3'>
<div className='flex items-center'>
{
!isInWebApp && !isInstalledApp && !isResponsing && (
<PromptLog
log={promptLog}
containerRef={ref}
>
{
showModal => (
<SimpleBtn
isDisabled={isError || !messageId}
className={cn(isMobile && '!px-1.5', 'space-x-1 mr-2')}
onClick={() => handleOpenLogModal(showModal)}>
<File02 className='w-3.5 h-3.5' />
{!isMobile && <div>{t('common.operation.log')}</div>}
</SimpleBtn>
)
}
</PromptLog>
)
}
<SimpleBtn
isDisabled={isError || !messageId}
className={cn(isMobile && '!px-1.5', 'space-x-1')}
onClick={() => {
copy(content)
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}}>
<Clipboard className='w-3.5 h-3.5' />
{!isMobile && <div>{t('common.operation.copy')}</div>}
</SimpleBtn>
{isInWebApp && (
const ratingContent = (
<>
<SimpleBtn
isDisabled={isError || !messageId}
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
onClick={() => { onSave?.(messageId as string) }}
>
<Bookmark className='w-3.5 h-3.5' />
{!isMobile && <div>{t('common.operation.save')}</div>}
</SimpleBtn>
{(moreLikeThis && depth < MAX_DEPTH) && (
<SimpleBtn
isDisabled={isError || !messageId}
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
onClick={handleMoreLikeThis}
>
<Stars02 className='w-3.5 h-3.5' />
{!isMobile && <div>{t('appDebug.feature.moreLikeThis.title')}</div>}
</SimpleBtn>)}
{isError && <SimpleBtn
onClick={onRetry}
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
>
<RefreshCcw01 className='w-3.5 h-3.5' />
{!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">
<>
@ -308,7 +229,145 @@ const GenerationItem: FC<IGenerationItemProps> = ({
</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
? {
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
}
: {}}
>
{isLoading
? (
<div className='flex items-center h-10'><Loading type='area' /></div>
)
: (
<div
className={cn(!isTop && 'rounded-br-xl border-l-2 border-primary-400', 'p-4')}
style={mainStyle}
>
{(isTop && taskId) && (
<div className='mb-2 text-gray-500 border border-gray-200 box-border flex items-center rounded-md italic text-[11px] pl-1 pr-1.5 font-medium w-fit group-hover:opacity-100'>
<HashtagIcon className='w-3 h-3 text-gray-400 fill-current mr-1 stroke-current stroke-1' />
{taskId}
</div>)
}
<div className='flex'>
<div className='grow w-0'>
{isError
? <div className='text-gray-400 text-sm'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
: (
<Markdown content={content} />
)}
</div>
</div>
<div className='flex items-center justify-between mt-3'>
<div className='flex items-center'>
{
!isInWebApp && !isInstalledApp && !isResponsing && (
<PromptLog
log={promptLog}
containerRef={ref}
>
{
showModal => (
<SimpleBtn
isDisabled={isError || !messageId}
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>}
</SimpleBtn>
)
}
</PromptLog>
)
}
<SimpleBtn
isDisabled={isError || !messageId}
className={cn(isMobile && '!px-1.5', 'space-x-1')}
onClick={() => {
copy(content)
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}}>
<Clipboard className='w-3.5 h-3.5' />
{!isMobile && <div>{t('common.operation.copy')}</div>}
</SimpleBtn>
{isInWebApp && (
<>
<SimpleBtn
isDisabled={isError || !messageId}
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
onClick={() => { onSave?.(messageId as string) }}
>
<Bookmark className='w-3.5 h-3.5' />
{!isMobile && <div>{t('common.operation.save')}</div>}
</SimpleBtn>
{(moreLikeThis && depth < MAX_DEPTH) && (
<SimpleBtn
isDisabled={isError || !messageId}
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
onClick={handleMoreLikeThis}
>
<Stars02 className='w-3.5 h-3.5' />
{!isMobile && <div>{t('appDebug.feature.moreLikeThis.title')}</div>}
</SimpleBtn>)}
{isError && <SimpleBtn
onClick={onRetry}
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
>
<RefreshCcw01 className='w-3.5 h-3.5' />
{!isMobile && <div>{t('share.generation.batchFailed.retry')}</div>}
</SimpleBtn>}
{!isError && messageId && <div className="mx-3 w-[1px] h-[14px] bg-gray-200"></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>

View 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)

View File

@ -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">

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

File diff suppressed because one or more lines are too long

View 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

View 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"
}

View 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

View File

@ -0,0 +1,2 @@
export { default as Robot } from './Robot'
export { default as User } from './User'

View File

@ -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"

View File

@ -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"
}

View File

@ -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

View File

@ -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"
}

View File

@ -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

View File

@ -1 +1,3 @@
export { default as ChatBot } from './ChatBot'
export { default as MessageCheckRemove } from './MessageCheckRemove'
export { default as MessageFastPlus } from './MessageFastPlus'

View File

@ -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"
}

View File

@ -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

View File

@ -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'

View File

@ -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"
}

View File

@ -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

View File

@ -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'

View File

@ -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"
}

View File

@ -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

View File

@ -0,0 +1 @@
export { default as ClockFastForward } from './ClockFastForward'

View File

@ -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"
}

View File

@ -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

View File

@ -0,0 +1 @@
export { default as MessageFast } from './MessageFast'

View File

@ -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"
}

View File

@ -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

View File

@ -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'

View File

@ -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}

View 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)

View File

@ -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;
}

View File

@ -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>

View 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)

View 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)

View 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)

View File

@ -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;
}

View 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)

View File

@ -63,10 +63,12 @@ export const defaultPlan = {
vectorSpace: 1,
buildApps: 1,
teamMembers: 1,
annotatedResponse: 1,
},
total: {
vectorSpace: 10,
buildApps: 10,
teamMembers: 1,
annotatedResponse: 10,
},
}

View File

@ -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={{

View File

@ -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
}

View File

@ -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),
},
}
}

View File

@ -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

View File

@ -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

View File

@ -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

Some files were not shown because too many files have changed in this diff Show More