feat: annotation management frontend (#1764)
This commit is contained in:
@@ -2,26 +2,26 @@
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { UserCircleIcon } from '@heroicons/react/24/solid'
|
||||
import cn from 'classnames'
|
||||
import type { CitationItem, DisplayScene, FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc, ThoughtItem } from '../type'
|
||||
import type { CitationItem, DisplayScene, FeedbackFunc, Feedbacktype, IChatItem, ThoughtItem } from '../type'
|
||||
import OperationBtn from '../operation'
|
||||
import LoadingAnim from '../loading-anim'
|
||||
import { EditIcon, EditIconSolid, OpeningStatementIcon, RatingIcon } from '../icon-component'
|
||||
import { EditIconSolid, OpeningStatementIcon, RatingIcon } from '../icon-component'
|
||||
import s from '../style.module.css'
|
||||
import MoreInfo from '../more-info'
|
||||
import CopyBtn from '../copy-btn'
|
||||
import Thought from '../thought'
|
||||
import Citation from '../citation'
|
||||
import { randomString } from '@/utils'
|
||||
import type { Annotation, MessageRating } from '@/models/log'
|
||||
import AppContext from '@/context/app-context'
|
||||
import type { MessageRating } from '@/models/log'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import AutoHeightTextarea from '@/app/components/base/auto-height-textarea'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import AnnotationCtrlBtn from '@/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn'
|
||||
import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
|
||||
import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
|
||||
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
|
||||
const Divider: FC<{ name: string }> = ({ name }) => {
|
||||
const { t } = useTranslation()
|
||||
@@ -42,7 +42,6 @@ export type IAnswerProps = {
|
||||
feedbackDisabled: boolean
|
||||
isHideFeedbackEdit: boolean
|
||||
onFeedback?: FeedbackFunc
|
||||
onSubmitAnnotation?: SubmitAnnotationFunc
|
||||
displayScene: DisplayScene
|
||||
isResponsing?: boolean
|
||||
answerIcon?: ReactNode
|
||||
@@ -52,6 +51,13 @@ export type IAnswerProps = {
|
||||
dataSets?: DataSet[]
|
||||
isShowCitation?: boolean
|
||||
isShowCitationHitInfo?: boolean
|
||||
// Annotation props
|
||||
supportAnnotation?: boolean
|
||||
appId?: string
|
||||
question: string
|
||||
onAnnotationEdited?: (question: string, answer: string) => void
|
||||
onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string) => void
|
||||
onAnnotationRemoved?: () => void
|
||||
}
|
||||
// The component needs to maintain its own state to control whether to display input component
|
||||
const Answer: FC<IAnswerProps> = ({
|
||||
@@ -59,7 +65,6 @@ const Answer: FC<IAnswerProps> = ({
|
||||
feedbackDisabled = false,
|
||||
isHideFeedbackEdit = false,
|
||||
onFeedback,
|
||||
onSubmitAnnotation,
|
||||
displayScene = 'web',
|
||||
isResponsing,
|
||||
answerIcon,
|
||||
@@ -69,15 +74,25 @@ const Answer: FC<IAnswerProps> = ({
|
||||
dataSets,
|
||||
isShowCitation,
|
||||
isShowCitationHitInfo = false,
|
||||
supportAnnotation,
|
||||
appId,
|
||||
question,
|
||||
onAnnotationEdited,
|
||||
onAnnotationAdded,
|
||||
onAnnotationRemoved,
|
||||
}) => {
|
||||
const { id, content, more, feedback, adminFeedback, annotation: initAnnotation } = item
|
||||
const { id, content, more, feedback, adminFeedback, annotation } = item
|
||||
const hasAnnotation = !!annotation?.id
|
||||
const [showEdit, setShowEdit] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [annotation, setAnnotation] = useState<Annotation | undefined | null>(initAnnotation)
|
||||
const [inputValue, setInputValue] = useState<string>(initAnnotation?.content ?? '')
|
||||
// const [annotation, setAnnotation] = useState<Annotation | undefined | null>(initAnnotation)
|
||||
// const [inputValue, setInputValue] = useState<string>(initAnnotation?.content ?? '')
|
||||
const [localAdminFeedback, setLocalAdminFeedback] = useState<Feedbacktype | undefined | null>(adminFeedback)
|
||||
const { userProfile } = useContext(AppContext)
|
||||
// const { userProfile } = useContext(AppContext)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isShowReplyModal, setIsShowReplyModal] = useState(false)
|
||||
|
||||
/**
|
||||
* Render feedback results (distinguish between users and administrators)
|
||||
* User reviews cannot be cancelled in Console
|
||||
@@ -121,6 +136,19 @@ const Answer: FC<IAnswerProps> = ({
|
||||
)
|
||||
}
|
||||
|
||||
const renderHasAnnotationBtn = () => {
|
||||
return (
|
||||
<div
|
||||
className={cn(s.hasAnnotationBtn, 'relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7]')}
|
||||
style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }}
|
||||
>
|
||||
<div className='p-1 rounded-lg bg-[#EEF4FF] '>
|
||||
<MessageFast className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Different scenarios have different operation items.
|
||||
* @param isWebScene Whether it is web scene
|
||||
@@ -142,12 +170,6 @@ const Answer: FC<IAnswerProps> = ({
|
||||
|
||||
const adminOperation = () => {
|
||||
return <div className='flex gap-1'>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.addAnnotation') as string}>
|
||||
{OperationBtn({
|
||||
innerContent: <IconWrapper><EditIcon className='hover:text-gray-800' /></IconWrapper>,
|
||||
onClick: () => setShowEdit(true),
|
||||
})}
|
||||
</Tooltip>
|
||||
{!localAdminFeedback?.rating && <>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.like') as string}>
|
||||
{OperationBtn({
|
||||
@@ -219,47 +241,27 @@ const Answer: FC<IAnswerProps> = ({
|
||||
)
|
||||
: (
|
||||
<div>
|
||||
<Markdown content={content} />
|
||||
{annotation?.logAnnotation && (
|
||||
<div className='mb-1'>
|
||||
<div className='mb-3'>
|
||||
<Markdown className='line-through !text-gray-400' content={content} />
|
||||
</div>
|
||||
<EditTitle title={t('appAnnotation.editBy', {
|
||||
author: annotation?.logAnnotation.account.name,
|
||||
})} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Markdown content={annotation?.logAnnotation ? annotation?.logAnnotation.content : content} />
|
||||
</div>
|
||||
{(hasAnnotation && !annotation?.logAnnotation) && (
|
||||
<EditTitle className='mt-1' title={t('appAnnotation.editBy', {
|
||||
author: annotation.authorName,
|
||||
})} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!showEdit
|
||||
? (annotation?.content
|
||||
&& <>
|
||||
<Divider name={annotation?.account?.name || userProfile?.name} />
|
||||
{annotation.content}
|
||||
</>)
|
||||
: <>
|
||||
<Divider name={annotation?.account?.name || userProfile?.name} />
|
||||
<AutoHeightTextarea
|
||||
placeholder={t('appLog.detail.operation.annotationPlaceholder') as string}
|
||||
value={inputValue}
|
||||
onChange={e => setInputValue(e.target.value)}
|
||||
minHeight={58}
|
||||
className={`${cn(s.textArea)} !py-2 resize-none block w-full !px-3 bg-gray-50 border border-gray-200 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm text-gray-700 tracking-[0.2px]`}
|
||||
/>
|
||||
<div className="mt-2 flex flex-row">
|
||||
<Button
|
||||
type='primary'
|
||||
className='mr-2'
|
||||
loading={loading}
|
||||
onClick={async () => {
|
||||
if (!inputValue)
|
||||
return
|
||||
setLoading(true)
|
||||
const res = await onSubmitAnnotation?.(id, inputValue)
|
||||
if (res)
|
||||
setAnnotation({ ...annotation, content: inputValue } as Annotation)
|
||||
setLoading(false)
|
||||
setShowEdit(false)
|
||||
}}>{t('common.operation.confirm')}</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setInputValue(annotation?.content ?? '')
|
||||
setShowEdit(false)
|
||||
}}>{t('common.operation.cancel')}</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
{
|
||||
!!citation?.length && !isThinking && isShowCitation && !isResponsing && (
|
||||
<Citation data={citation} showHitInfo={isShowCitationHitInfo} />
|
||||
@@ -273,6 +275,36 @@ const Answer: FC<IAnswerProps> = ({
|
||||
className={cn(s.copyBtn, 'mr-1')}
|
||||
/>
|
||||
)}
|
||||
{supportAnnotation && (
|
||||
<AnnotationCtrlBtn
|
||||
appId={appId!}
|
||||
messageId={id}
|
||||
annotationId={annotation?.id || ''}
|
||||
className={cn(s.annotationBtn, 'ml-1')}
|
||||
cached={hasAnnotation}
|
||||
query={question}
|
||||
answer={content}
|
||||
onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content)}
|
||||
onEdit={() => setIsShowReplyModal(true)}
|
||||
onRemoved={onAnnotationRemoved!}
|
||||
/>
|
||||
)}
|
||||
|
||||
<EditReplyModal
|
||||
isShow={isShowReplyModal}
|
||||
onHide={() => setIsShowReplyModal(false)}
|
||||
query={question}
|
||||
answer={content}
|
||||
onEdited={onAnnotationEdited!}
|
||||
onAdded={onAnnotationAdded!}
|
||||
appId={appId!}
|
||||
messageId={id}
|
||||
annotationId={annotation?.id || ''}
|
||||
createdAt={annotation?.created_at}
|
||||
onRemove={() => { }}
|
||||
/>
|
||||
{hasAnnotation && renderHasAnnotationBtn()}
|
||||
|
||||
{!feedbackDisabled && !item.feedbackDisabled && renderItemOperation(displayScene !== 'console')}
|
||||
{/* Admin feedback is displayed only in the background. */}
|
||||
{!feedbackDisabled && renderFeedbackRating(localAdminFeedback?.rating, false, false)}
|
||||
@@ -280,6 +312,7 @@ const Answer: FC<IAnswerProps> = ({
|
||||
{!feedbackDisabled && renderFeedbackRating(feedback?.rating, !isHideFeedbackEdit, displayScene !== 'console')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{more && <MoreInfo className='invisible group-hover:visible' more={more} isQuestion={false} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ import cn from 'classnames'
|
||||
import Recorder from 'js-audio-recorder'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import s from './style.module.css'
|
||||
import type { DisplayScene, FeedbackFunc, IChatItem, SubmitAnnotationFunc } from './type'
|
||||
import type { DisplayScene, FeedbackFunc, IChatItem } from './type'
|
||||
import { TryToAskIcon, stopIcon } from './icon-component'
|
||||
import Answer from './answer'
|
||||
import Question from './question'
|
||||
@@ -24,10 +24,13 @@ import ChatImageUploader from '@/app/components/base/image-uploader/chat-image-u
|
||||
import ImageList from '@/app/components/base/image-uploader/image-list'
|
||||
import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app'
|
||||
import { useClipboardUploader, useDraggableUploader, useImageFiles } from '@/app/components/base/image-uploader/hooks'
|
||||
import type { Annotation } from '@/models/log'
|
||||
|
||||
export type IChatProps = {
|
||||
appId?: string
|
||||
configElem?: React.ReactNode
|
||||
chatList: IChatItem[]
|
||||
onChatListChange?: (chatList: IChatItem[]) => void
|
||||
controlChatUpdateAllConversation?: number
|
||||
/**
|
||||
* Whether to display the editing area and rating status
|
||||
@@ -39,7 +42,6 @@ export type IChatProps = {
|
||||
isHideFeedbackEdit?: boolean
|
||||
isHideSendInput?: boolean
|
||||
onFeedback?: FeedbackFunc
|
||||
onSubmitAnnotation?: SubmitAnnotationFunc
|
||||
checkCanSend?: () => boolean
|
||||
onSend?: (message: string, files: VisionFile[]) => void
|
||||
displayScene?: DisplayScene
|
||||
@@ -59,6 +61,7 @@ export type IChatProps = {
|
||||
isShowCitationHitInfo?: boolean
|
||||
isShowPromptLog?: boolean
|
||||
visionConfig?: VisionSettings
|
||||
supportAnnotation?: boolean
|
||||
}
|
||||
|
||||
const Chat: FC<IChatProps> = ({
|
||||
@@ -69,7 +72,6 @@ const Chat: FC<IChatProps> = ({
|
||||
isHideFeedbackEdit = false,
|
||||
isHideSendInput = false,
|
||||
onFeedback,
|
||||
onSubmitAnnotation,
|
||||
checkCanSend,
|
||||
onSend = () => { },
|
||||
displayScene,
|
||||
@@ -89,6 +91,9 @@ const Chat: FC<IChatProps> = ({
|
||||
isShowCitationHitInfo,
|
||||
isShowPromptLog,
|
||||
visionConfig,
|
||||
appId,
|
||||
supportAnnotation,
|
||||
onChatListChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
@@ -190,7 +195,7 @@ const Chat: FC<IChatProps> = ({
|
||||
{isShowConfigElem && (configElem || null)}
|
||||
{/* Chat List */}
|
||||
<div className={cn((isShowConfigElem && configElem) ? 'h-0' : 'h-full', 'space-y-[30px]')}>
|
||||
{chatList.map((item) => {
|
||||
{chatList.map((item, index) => {
|
||||
if (item.isAnswer) {
|
||||
const isLast = item.id === chatList[chatList.length - 1].id
|
||||
const thoughts = item.agent_thoughts?.filter(item => item.thought !== '[DONE]')
|
||||
@@ -202,7 +207,6 @@ const Chat: FC<IChatProps> = ({
|
||||
feedbackDisabled={feedbackDisabled}
|
||||
isHideFeedbackEdit={isHideFeedbackEdit}
|
||||
onFeedback={onFeedback}
|
||||
onSubmitAnnotation={onSubmitAnnotation}
|
||||
displayScene={displayScene ?? 'web'}
|
||||
isResponsing={isResponsing && isLast}
|
||||
answerIcon={answerIcon}
|
||||
@@ -212,6 +216,72 @@ const Chat: FC<IChatProps> = ({
|
||||
dataSets={dataSets}
|
||||
isShowCitation={isShowCitation}
|
||||
isShowCitationHitInfo={isShowCitationHitInfo}
|
||||
supportAnnotation={supportAnnotation}
|
||||
appId={appId}
|
||||
question={chatList[index - 1]?.content}
|
||||
onAnnotationEdited={(query, answer) => {
|
||||
onChatListChange?.(chatList.map((item, i) => {
|
||||
if (i === index - 1) {
|
||||
return {
|
||||
...item,
|
||||
content: query,
|
||||
}
|
||||
}
|
||||
if (i === index) {
|
||||
return {
|
||||
...item,
|
||||
content: answer,
|
||||
}
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}}
|
||||
onAnnotationAdded={(annotationId, authorName, query, answer) => {
|
||||
onChatListChange?.(chatList.map((item, i) => {
|
||||
if (i === index - 1) {
|
||||
return {
|
||||
...item,
|
||||
content: query,
|
||||
}
|
||||
}
|
||||
if (i === index) {
|
||||
const answerItem = {
|
||||
...item,
|
||||
content: item.content,
|
||||
annotation: {
|
||||
id: annotationId,
|
||||
authorName,
|
||||
logAnnotation: {
|
||||
content: answer,
|
||||
account: {
|
||||
id: '',
|
||||
name: authorName,
|
||||
email: '',
|
||||
},
|
||||
},
|
||||
} as Annotation,
|
||||
}
|
||||
return answerItem
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}}
|
||||
onAnnotationRemoved={() => {
|
||||
onChatListChange?.(chatList.map((item, i) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
...item,
|
||||
content: item.content,
|
||||
annotation: {
|
||||
...(item.annotation || {}),
|
||||
id: '',
|
||||
} as Annotation,
|
||||
}
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}}
|
||||
|
||||
/>
|
||||
}
|
||||
return (
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
background: url(./icons/answer.svg) no-repeat;
|
||||
}
|
||||
|
||||
.copyBtn {
|
||||
.copyBtn,
|
||||
.annotationBtn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -63,10 +64,15 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.answerWrap:hover .copyBtn {
|
||||
.answerWrap:hover .copyBtn,
|
||||
.answerWrap:hover .annotationBtn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.answerWrap:hover .hasAnnotationBtn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.answerWrap .itemOperation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -81,3 +81,12 @@ export type MessageReplace = {
|
||||
answer: string
|
||||
conversation_id: string
|
||||
}
|
||||
|
||||
export type AnnotationReply = {
|
||||
id: string
|
||||
task_id: string
|
||||
answer: string
|
||||
conversation_id: string
|
||||
annotation_id: string
|
||||
annotation_author_name: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user