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
122 changed files with 4718 additions and 214 deletions

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

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
}