瀏覽代碼

feat: annotation management frontend (#1764)

Joel 1 年之前
父節點
當前提交
65fd4b39ce
共有 100 個文件被更改,包括 4276 次插入181 次删除
  1. 17 0
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/annotations/page.tsx
  2. 3 2
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/logs/page.tsx
  3. 9 1
      web/app/components/app-sidebar/navLink.tsx
  4. 47 0
      web/app/components/app/annotation/add-annotation-modal/edit-item/index.tsx
  5. 120 0
      web/app/components/app/annotation/add-annotation-modal/index.tsx
  6. 74 0
      web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.tsx
  7. 126 0
      web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx
  8. 124 0
      web/app/components/app/annotation/batch-add-annotation-modal/index.tsx
  9. 131 0
      web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx
  10. 142 0
      web/app/components/app/annotation/edit-annotation-modal/index.tsx
  11. 26 0
      web/app/components/app/annotation/empty-element.tsx
  12. 54 0
      web/app/components/app/annotation/filter.tsx
  13. 141 0
      web/app/components/app/annotation/header-opts/index.tsx
  14. 32 0
      web/app/components/app/annotation/header-opts/style.module.css
  15. 315 0
      web/app/components/app/annotation/index.tsx
  16. 98 0
      web/app/components/app/annotation/list.tsx
  17. 29 0
      web/app/components/app/annotation/remove-annotation-confirm-modal/index.tsx
  18. 9 0
      web/app/components/app/annotation/style.module.css
  19. 39 0
      web/app/components/app/annotation/type.ts
  20. 19 0
      web/app/components/app/annotation/view-annotation-modal/hit-history-no-data.tsx
  21. 237 0
      web/app/components/app/annotation/view-annotation-modal/index.tsx
  22. 9 0
      web/app/components/app/annotation/view-annotation-modal/style.module.css
  23. 91 58
      web/app/components/app/chat/answer/index.tsx
  24. 75 5
      web/app/components/app/chat/index.tsx
  25. 8 2
      web/app/components/app/chat/style.module.css
  26. 9 0
      web/app/components/app/chat/type.ts
  27. 11 2
      web/app/components/app/configuration/config/feature/choose-feature/index.tsx
  28. 8 0
      web/app/components/app/configuration/config/feature/use-feature.tsx
  29. 69 12
      web/app/components/app/configuration/config/index.tsx
  30. 9 2
      web/app/components/app/configuration/dataset-config/context-var/var-picker.tsx
  31. 36 2
      web/app/components/app/configuration/debug/index.tsx
  32. 0 1
      web/app/components/app/configuration/features/chat-group/index.tsx
  33. 27 6
      web/app/components/app/configuration/index.tsx
  34. 132 0
      web/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn/index.tsx
  35. 138 0
      web/app/components/app/configuration/toolbox/annotation/config-param-modal.tsx
  36. 124 0
      web/app/components/app/configuration/toolbox/annotation/config-param.tsx
  37. 4 0
      web/app/components/app/configuration/toolbox/annotation/type.ts
  38. 89 0
      web/app/components/app/configuration/toolbox/annotation/use-annotation-config.ts
  39. 19 1
      web/app/components/app/configuration/toolbox/index.tsx
  40. 38 0
      web/app/components/app/configuration/toolbox/score-slider/base-slider/index.tsx
  41. 20 0
      web/app/components/app/configuration/toolbox/score-slider/base-slider/style.module.css
  42. 46 0
      web/app/components/app/configuration/toolbox/score-slider/index.tsx
  43. 45 0
      web/app/components/app/log-annotation/index.tsx
  44. 8 14
      web/app/components/app/log/index.tsx
  45. 45 13
      web/app/components/app/log/list.tsx
  46. 107 48
      web/app/components/app/text-generate/item/index.tsx
  47. 69 0
      web/app/components/base/drawer-plus/index.tsx
  48. 3 1
      web/app/components/base/drawer/index.tsx
  49. 8 0
      web/app/components/base/icons/assets/public/avatar/robot.svg
  50. 12 0
      web/app/components/base/icons/assets/public/avatar/user.svg
  51. 2 2
      web/app/components/base/icons/assets/public/billing/sparkles.svg
  52. 5 0
      web/app/components/base/icons/assets/vender/line/communication/message-check-remove.svg
  53. 3 0
      web/app/components/base/icons/assets/vender/line/communication/message-fast-plus.svg
  54. 3 0
      web/app/components/base/icons/assets/vender/line/files/file-download-02.svg
  55. 3 0
      web/app/components/base/icons/assets/vender/line/general/edit-04.svg
  56. 3 0
      web/app/components/base/icons/assets/vender/line/time/clock-fast-forward.svg
  57. 1 0
      web/app/components/base/icons/assets/vender/solid/communication/message-fast.svg
  58. 4 0
      web/app/components/base/icons/assets/vender/solid/general/edit-04.svg
  59. 82 0
      web/app/components/base/icons/src/public/avatar/Robot.json
  60. 16 0
      web/app/components/base/icons/src/public/avatar/Robot.tsx
  61. 89 0
      web/app/components/base/icons/src/public/avatar/User.json
  62. 16 0
      web/app/components/base/icons/src/public/avatar/User.tsx
  63. 2 0
      web/app/components/base/icons/src/public/avatar/index.ts
  64. 2 2
      web/app/components/base/icons/src/public/billing/Sparkles.json
  65. 39 0
      web/app/components/base/icons/src/vender/line/communication/MessageCheckRemove.json
  66. 16 0
      web/app/components/base/icons/src/vender/line/communication/MessageCheckRemove.tsx
  67. 29 0
      web/app/components/base/icons/src/vender/line/communication/MessageFastPlus.json
  68. 16 0
      web/app/components/base/icons/src/vender/line/communication/MessageFastPlus.tsx
  69. 2 0
      web/app/components/base/icons/src/vender/line/communication/index.ts
  70. 29 0
      web/app/components/base/icons/src/vender/line/files/FileDownload02.json
  71. 16 0
      web/app/components/base/icons/src/vender/line/files/FileDownload02.tsx
  72. 1 0
      web/app/components/base/icons/src/vender/line/files/index.ts
  73. 29 0
      web/app/components/base/icons/src/vender/line/general/Edit04.json
  74. 16 0
      web/app/components/base/icons/src/vender/line/general/Edit04.tsx
  75. 1 0
      web/app/components/base/icons/src/vender/line/general/index.ts
  76. 29 0
      web/app/components/base/icons/src/vender/line/time/ClockFastForward.json
  77. 16 0
      web/app/components/base/icons/src/vender/line/time/ClockFastForward.tsx
  78. 1 0
      web/app/components/base/icons/src/vender/line/time/index.ts
  79. 19 0
      web/app/components/base/icons/src/vender/solid/communication/MessageFast.json
  80. 16 0
      web/app/components/base/icons/src/vender/solid/communication/MessageFast.tsx
  81. 1 0
      web/app/components/base/icons/src/vender/solid/communication/index.ts
  82. 39 0
      web/app/components/base/icons/src/vender/solid/general/Edit04.json
  83. 16 0
      web/app/components/base/icons/src/vender/solid/general/Edit04.tsx
  84. 1 0
      web/app/components/base/icons/src/vender/solid/general/index.ts
  85. 3 3
      web/app/components/base/markdown.tsx
  86. 66 0
      web/app/components/base/modal/delete-confirm-modal/index.tsx
  87. 16 0
      web/app/components/base/modal/delete-confirm-modal/style.module.css
  88. 7 2
      web/app/components/base/modal/index.tsx
  89. 68 0
      web/app/components/base/tab-slider-plain/index.tsx
  90. 31 0
      web/app/components/billing/annotation-full/index.tsx
  91. 47 0
      web/app/components/billing/annotation-full/modal.tsx
  92. 7 0
      web/app/components/billing/annotation-full/style.module.css
  93. 32 0
      web/app/components/billing/annotation-full/usage.tsx
  94. 2 0
      web/app/components/billing/config.ts
  95. 1 1
      web/app/components/billing/progress-bar/index.tsx
  96. 5 1
      web/app/components/billing/type.ts
  97. 2 0
      web/app/components/billing/utils/index.ts
  98. 358 0
      web/app/components/header/account-setting/model-page/model-selector/portal-select.tsx
  99. 16 0
      web/app/components/share/chat/index.tsx
  100. 1 0
      web/app/components/share/text-generation/result/index.tsx

+ 17 - 0
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/annotations/page.tsx

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

+ 3 - 2
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/logs/page.tsx

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

+ 9 - 1
web/app/components/app-sidebar/navLink.tsx

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

+ 47 - 0
web/app/components/app/annotation/add-annotation-modal/edit-item/index.tsx

@@ -0,0 +1,47 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import Textarea from 'rc-textarea'
+import { Robot, User } from '@/app/components/base/icons/src/public/avatar'
+
+export enum EditItemType {
+  Query = 'query',
+  Answer = 'answer',
+}
+type Props = {
+  type: EditItemType
+  content: string
+  onChange: (content: string) => void
+}
+
+const EditItem: FC<Props> = ({
+  type,
+  content,
+  onChange,
+}) => {
+  const { t } = useTranslation()
+  const avatar = type === EditItemType.Query ? <User className='w-6 h-6' /> : <Robot className='w-6 h-6' />
+  const name = type === EditItemType.Query ? t('appAnnotation.addModal.queryName') : t('appAnnotation.addModal.answerName')
+  const placeholder = type === EditItemType.Query ? t('appAnnotation.addModal.queryPlaceholder') : t('appAnnotation.addModal.answerPlaceholder')
+
+  return (
+    <div className='flex' onClick={e => e.stopPropagation()}>
+      <div className='shrink-0 mr-3'>
+        {avatar}
+      </div>
+      <div className='grow'>
+        <div className='mb-1 leading-[18px] text-xs font-semibold text-gray-900'>{name}</div>
+        <Textarea
+          className='mt-1 block w-full leading-5 max-h-none text-sm text-gray-700 outline-none appearance-none resize-none'
+          value={content}
+          onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)}
+          autoSize={{ minRows: 3 }}
+          placeholder={placeholder}
+          autoFocus
+        />
+      </div>
+    </div>
+  )
+}
+export default React.memo(EditItem)

+ 120 - 0
web/app/components/app/annotation/add-annotation-modal/index.tsx

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

+ 74 - 0
web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.tsx

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

+ 126 - 0
web/app/components/app/annotation/batch-add-annotation-modal/csv-uploader.tsx

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

+ 124 - 0
web/app/components/app/annotation/batch-add-annotation-modal/index.tsx

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

+ 131 - 0
web/app/components/app/annotation/edit-annotation-modal/edit-item/index.tsx

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

+ 142 - 0
web/app/components/app/annotation/edit-annotation-modal/index.tsx

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

+ 26 - 0
web/app/components/app/annotation/empty-element.tsx

@@ -0,0 +1,26 @@
+'use client'
+import type { FC, SVGProps } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+
+const ThreeDotsIcon = ({ className }: SVGProps<SVGElement>) => {
+  return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
+    <path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
+  </svg>
+}
+
+const EmptyElement: FC = () => {
+  const { t } = useTranslation()
+
+  return (
+    <div className='flex items-center justify-center h-full'>
+      <div className='bg-gray-50 w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
+        <span className='text-gray-700 font-semibold'>{t('appAnnotation.noData.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
+        <div className='mt-2 text-gray-500 text-sm font-normal'>
+          {t('appAnnotation.noData.description')}
+        </div>
+      </div>
+    </div>
+  )
+}
+export default React.memo(EmptyElement)

+ 54 - 0
web/app/components/app/annotation/filter.tsx

@@ -0,0 +1,54 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+  MagnifyingGlassIcon,
+} from '@heroicons/react/24/solid'
+import useSWR from 'swr'
+import { fetchAnnotationsCount } from '@/service/log'
+
+export type QueryParam = {
+  keyword?: string
+}
+
+type IFilterProps = {
+  appId: string
+  queryParams: QueryParam
+  setQueryParams: (v: QueryParam) => void
+  children: JSX.Element
+}
+
+const Filter: FC<IFilterProps> = ({
+  appId,
+  queryParams,
+  setQueryParams,
+  children,
+}) => {
+  // TODO: change fetch list api
+  const { data } = useSWR({ url: `/apps/${appId}/annotations/count` }, fetchAnnotationsCount)
+  const { t } = useTranslation()
+  if (!data)
+    return null
+  return (
+    <div className='flex justify-between flex-row flex-wrap gap-y-2 gap-x-4 items-center mb-4 text-gray-900 text-base'>
+      <div className="relative">
+        <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
+          <MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
+        </div>
+        <input
+          type="text"
+          name="query"
+          className="block w-[240px] bg-gray-100 shadow-sm rounded-md border-0 py-1.5 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-6"
+          placeholder={t('common.operation.search') as string}
+          value={queryParams.keyword}
+          onChange={(e) => {
+            setQueryParams({ ...queryParams, keyword: e.target.value })
+          }}
+        />
+      </div>
+      {children}
+    </div>
+  )
+}
+export default React.memo(Filter)

+ 141 - 0
web/app/components/app/annotation/header-opts/index.tsx

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

+ 32 - 0
web/app/components/app/annotation/header-opts/style.module.css

@@ -0,0 +1,32 @@
+.actionIconWrapper {
+  @apply h-8 w-8 p-2 rounded-md hover:bg-gray-100 !important;
+}
+
+.commonIcon {
+  @apply w-4 h-4 inline-block align-middle;
+  background-repeat: no-repeat;
+  background-position: center center;
+  background-size: contain;
+}
+
+.actionIcon {
+  @apply bg-gray-500;
+  mask-image: url(~@/assets/action.svg);
+}
+
+.actionItemIcon {
+  @apply w-4 h-4 text-gray-500;
+}
+
+.actionItem {
+  @apply h-9 py-2 px-3 mx-1 flex items-center space-x-2 hover:bg-gray-100 rounded-lg cursor-pointer;
+  width: calc(100% - 0.5rem);
+}
+
+.deleteActionItem {
+  @apply hover:bg-red-50 !important;
+}
+
+.actionName {
+  @apply text-gray-700 text-sm;
+}

+ 315 - 0
web/app/components/app/annotation/index.tsx

@@ -0,0 +1,315 @@
+'use client'
+import type { FC } from 'react'
+import React, { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { Pagination } from 'react-headless-pagination'
+import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline'
+import cn from 'classnames'
+import Toast from '../../base/toast'
+import Filter from './filter'
+import type { QueryParam } from './filter'
+import List from './list'
+import EmptyElement from './empty-element'
+import HeaderOpts from './header-opts'
+import s from './style.module.css'
+import { AnnotationEnableStatus, type AnnotationItem, type AnnotationItemBasic, JobStatus } from './type'
+import ViewAnnotationModal from './view-annotation-modal'
+import Switch from '@/app/components/base/switch'
+import { addAnnotation, delAnnotation, fetchAnnotationConfig as doFetchAnnotationConfig, editAnnotation, fetchAnnotationList, queryAnnotationJobStatus, updateAnnotationScore, updateAnnotationStatus } from '@/service/annotation'
+import Loading from '@/app/components/base/loading'
+import { APP_PAGE_LIMIT } from '@/config'
+import ConfigParamModal from '@/app/components/app/configuration/toolbox/annotation/config-param-modal'
+import type { AnnotationReplyConfig } from '@/models/debug'
+import { sleep } from '@/utils'
+import { useProviderContext } from '@/context/provider-context'
+import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
+import { Settings04 } from '@/app/components/base/icons/src/vender/line/general'
+import { fetchAppDetail } from '@/service/apps'
+
+type Props = {
+  appId: string
+}
+
+const Annotation: FC<Props> = ({
+  appId,
+}) => {
+  const { t } = useTranslation()
+  const [isShowEdit, setIsShowEdit] = React.useState(false)
+  const [annotationConfig, setAnnotationConfig] = useState<AnnotationReplyConfig | null>(null)
+  const [isChatApp, setIsChatApp] = useState(false)
+
+  const fetchAnnotationConfig = async () => {
+    const res = await doFetchAnnotationConfig(appId)
+    setAnnotationConfig(res as AnnotationReplyConfig)
+  }
+  useEffect(() => {
+    fetchAppDetail({ url: '/apps', id: appId }).then(async (res: any) => {
+      const isChatApp = res.mode === 'chat'
+      setIsChatApp(isChatApp)
+      if (isChatApp)
+        fetchAnnotationConfig()
+    })
+  }, [])
+  const [controlRefreshSwitch, setControlRefreshSwitch] = useState(Date.now())
+  const { plan, enableBilling } = useProviderContext()
+  const isAnnotationFull = (enableBilling && plan.usage.annotatedResponse >= plan.total.annotatedResponse)
+  const [isShowAnnotationFullModal, setIsShowAnnotationFullModal] = useState(false)
+  const ensureJobCompleted = async (jobId: string, status: AnnotationEnableStatus) => {
+    let isCompleted = false
+    while (!isCompleted) {
+      const res: any = await queryAnnotationJobStatus(appId, status, jobId)
+      isCompleted = res.job_status === JobStatus.completed
+      if (isCompleted)
+        break
+
+      await sleep(2000)
+    }
+  }
+
+  const [queryParams, setQueryParams] = useState<QueryParam>({})
+  const [currPage, setCurrPage] = React.useState<number>(0)
+  const query = {
+    page: currPage + 1,
+    limit: APP_PAGE_LIMIT,
+    keyword: queryParams.keyword || '',
+  }
+
+  const [controlUpdateList, setControlUpdateList] = useState(Date.now())
+  const [list, setList] = useState<AnnotationItem[]>([])
+  const [total, setTotal] = useState(10)
+  const [isLoading, setIsLoading] = useState(false)
+  const fetchList = async (page = 1) => {
+    setIsLoading(true)
+    try {
+      const { data, total }: any = await fetchAnnotationList(appId, {
+        ...query,
+        page,
+      })
+      setList(data as AnnotationItem[])
+      setTotal(total)
+    }
+    catch (e) {
+
+    }
+    setIsLoading(false)
+  }
+
+  useEffect(() => {
+    fetchList(currPage + 1)
+  }, [currPage])
+
+  useEffect(() => {
+    fetchList(1)
+    setControlUpdateList(Date.now())
+  }, [queryParams])
+
+  const handleAdd = async (payload: AnnotationItemBasic) => {
+    await addAnnotation(appId, {
+      ...payload,
+    })
+    Toast.notify({
+      message: t('common.api.actionSuccess'),
+      type: 'success',
+    })
+    fetchList()
+    setControlUpdateList(Date.now())
+  }
+
+  const handleRemove = async (id: string) => {
+    await delAnnotation(appId, id)
+    Toast.notify({
+      message: t('common.api.actionSuccess'),
+      type: 'success',
+    })
+    fetchList()
+    setControlUpdateList(Date.now())
+  }
+
+  const [currItem, setCurrItem] = useState<AnnotationItem | null>(list[0])
+  const [isShowViewModal, setIsShowViewModal] = useState(false)
+  useEffect(() => {
+    if (!isShowEdit)
+      setControlRefreshSwitch(Date.now())
+  }, [isShowEdit])
+  const handleView = (item: AnnotationItem) => {
+    setCurrItem(item)
+    setIsShowViewModal(true)
+  }
+
+  const handleSave = async (question: string, answer: string) => {
+    await editAnnotation(appId, (currItem as AnnotationItem).id, {
+      question,
+      answer,
+    })
+    Toast.notify({
+      message: t('common.api.actionSuccess'),
+      type: 'success',
+    })
+    fetchList()
+    setControlUpdateList(Date.now())
+  }
+
+  return (
+    <div className='flex flex-col h-full'>
+      <p className='flex text-sm font-normal text-gray-500'>{t('appLog.description')}</p>
+      <div className='grow flex flex-col py-4 '>
+        <Filter appId={appId} queryParams={queryParams} setQueryParams={setQueryParams}>
+          <div className='flex items-center space-x-2'>
+            {isChatApp && (
+              <>
+                <div className={cn(!annotationConfig?.enabled && 'pr-2', 'flex items-center h-7 rounded-lg border border-gray-200 pl-2 space-x-1')}>
+                  <div className='leading-[18px] text-[13px] font-medium text-gray-900'>{t('appAnnotation.name')}</div>
+                  <Switch
+                    key={controlRefreshSwitch}
+                    defaultValue={annotationConfig?.enabled}
+                    size='md'
+                    onChange={async (value) => {
+                      if (value) {
+                        if (isAnnotationFull) {
+                          setIsShowAnnotationFullModal(true)
+                          setControlRefreshSwitch(Date.now())
+                          return
+                        }
+                        setIsShowEdit(true)
+                      }
+                      else {
+                        const { job_id: jobId }: any = await updateAnnotationStatus(appId, AnnotationEnableStatus.disable, annotationConfig?.embedding_model, annotationConfig?.score_threshold)
+                        await ensureJobCompleted(jobId, AnnotationEnableStatus.disable)
+                        await fetchAnnotationConfig()
+                        Toast.notify({
+                          message: t('common.api.actionSuccess'),
+                          type: 'success',
+                        })
+                      }
+                    }}
+                  ></Switch>
+                  {annotationConfig?.enabled && (
+                    <div className='flex items-center pl-1.5'>
+                      <div className='shrink-0 mr-1 w-[1px] h-3.5 bg-gray-200'></div>
+                      <div
+                        className={`
+                      shrink-0  h-7 w-7 flex items-center justify-center
+                      text-xs text-gray-700 font-medium 
+                    `}
+                        onClick={() => { setIsShowEdit(true) }}
+                      >
+                        <div className='flex h-6 w-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-200'>
+                          <Settings04 className='w-4 h-4' />
+                        </div>
+                      </div>
+                    </div>
+                  )}
+                </div>
+                <div className='shrink-0 mx-3 w-[1px] h-3.5 bg-gray-200'></div>
+              </>
+            )}
+
+            <HeaderOpts
+              appId={appId}
+              controlUpdateList={controlUpdateList}
+              onAdd={handleAdd}
+              onAdded={() => {
+                fetchList()
+              }}
+            />
+          </div>
+        </Filter>
+        {isLoading
+          ? <Loading type='app' />
+          : total > 0
+            ? <List
+              list={list}
+              onRemove={handleRemove}
+              onView={handleView}
+            />
+            : <div className='grow flex h-full items-center justify-center'><EmptyElement /></div>
+        }
+        {/* Show Pagination only if the total is more than the limit */}
+        {(total && total > APP_PAGE_LIMIT)
+          ? <Pagination
+            className="flex items-center w-full h-10 text-sm select-none mt-8"
+            currentPage={currPage}
+            edgePageCount={2}
+            middlePagesSiblingCount={1}
+            setCurrentPage={setCurrPage}
+            totalPages={Math.ceil(total / APP_PAGE_LIMIT)}
+            truncableClassName="w-8 px-0.5 text-center"
+            truncableText="..."
+          >
+            <Pagination.PrevButton
+              disabled={currPage === 0}
+              className={`flex items-center mr-2 text-gray-500  focus:outline-none ${currPage === 0 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} >
+              <ArrowLeftIcon className="mr-3 h-3 w-3" />
+              {t('appLog.table.pagination.previous')}
+            </Pagination.PrevButton>
+            <div className={`flex items-center justify-center flex-grow ${s.pagination}`}>
+              <Pagination.PageButton
+                activeClassName="bg-primary-50 dark:bg-opacity-0 text-primary-600 dark:text-white"
+                className="flex items-center justify-center h-8 w-8 rounded-full cursor-pointer"
+                inactiveClassName="text-gray-500"
+              />
+            </div>
+            <Pagination.NextButton
+              disabled={currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1}
+              className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} >
+              {t('appLog.table.pagination.next')}
+              <ArrowRightIcon className="ml-3 h-3 w-3" />
+            </Pagination.NextButton>
+          </Pagination>
+          : null}
+
+        {isShowViewModal && (
+          <ViewAnnotationModal
+            appId={appId}
+            isShow={isShowViewModal}
+            onHide={() => setIsShowViewModal(false)}
+            onRemove={async () => {
+              await handleRemove((currItem as AnnotationItem)?.id)
+            }}
+            item={currItem as AnnotationItem}
+            onSave={handleSave}
+          />
+        )}
+        {isShowEdit && (
+          <ConfigParamModal
+            appId={appId}
+            isShow
+            isInit={!annotationConfig?.enabled}
+            onHide={() => {
+              setIsShowEdit(false)
+            }}
+            onSave={async (embeddingModel, score) => {
+              if (
+                embeddingModel.embedding_model_name !== annotationConfig?.embedding_model?.embedding_model_name
+                && embeddingModel.embedding_provider_name !== annotationConfig?.embedding_model?.embedding_provider_name
+              ) {
+                const { job_id: jobId }: any = await updateAnnotationStatus(appId, AnnotationEnableStatus.enable, embeddingModel, score)
+                await ensureJobCompleted(jobId, AnnotationEnableStatus.enable)
+              }
+
+              if (score !== annotationConfig?.score_threshold)
+                await updateAnnotationScore(appId, annotationConfig?.id || '', score)
+
+              await fetchAnnotationConfig()
+              Toast.notify({
+                message: t('common.api.actionSuccess'),
+                type: 'success',
+              })
+              setIsShowEdit(false)
+            }}
+            annotationConfig={annotationConfig!}
+          />
+        )}
+        {
+          isShowAnnotationFullModal && (
+            <AnnotationFullModal
+              show={isShowAnnotationFullModal}
+              onHide={() => setIsShowAnnotationFullModal(false)}
+            />
+          )
+        }
+      </div>
+    </div>
+  )
+}
+export default React.memo(Annotation)

+ 98 - 0
web/app/components/app/annotation/list.tsx

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

+ 29 - 0
web/app/components/app/annotation/remove-annotation-confirm-modal/index.tsx

@@ -0,0 +1,29 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import DeleteConfirmModal from '@/app/components/base/modal/delete-confirm-modal'
+
+type Props = {
+  isShow: boolean
+  onHide: () => void
+  onRemove: () => void
+}
+
+const RemoveAnnotationConfirmModal: FC<Props> = ({
+  isShow,
+  onHide,
+  onRemove,
+}) => {
+  const { t } = useTranslation()
+
+  return (
+    <DeleteConfirmModal
+      isShow={isShow}
+      onHide={onHide}
+      onRemove={onRemove}
+      text={t('appDebug.feature.annotation.removeConfirm') as string}
+    />
+  )
+}
+export default React.memo(RemoveAnnotationConfirmModal)

+ 9 - 0
web/app/components/app/annotation/style.module.css

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

+ 39 - 0
web/app/components/app/annotation/type.ts

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

+ 19 - 0
web/app/components/app/annotation/view-annotation-modal/hit-history-no-data.tsx

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

+ 237 - 0
web/app/components/app/annotation/view-annotation-modal/index.tsx

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

+ 9 - 0
web/app/components/app/annotation/view-annotation-modal/style.module.css

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

+ 91 - 58
web/app/components/app/chat/answer/index.tsx

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

+ 75 - 5
web/app/components/app/chat/index.tsx

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

+ 8 - 2
web/app/components/app/chat/style.module.css

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

+ 9 - 0
web/app/components/app/chat/type.ts

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

+ 11 - 2
web/app/components/app/configuration/config/feature/choose-feature/index.tsx

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

+ 8 - 0
web/app/components/app/configuration/config/feature/use-feature.tsx

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

+ 69 - 12
web/app/components/app/configuration/config/index.tsx

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

+ 9 - 2
web/app/components/app/configuration/dataset-config/context-var/var-picker.tsx

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

+ 36 - 2
web/app/components/app/configuration/debug/index.tsx

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

+ 0 - 1
web/app/components/app/configuration/features/chat-group/index.tsx

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

+ 27 - 6
web/app/components/app/configuration/index.tsx

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

+ 132 - 0
web/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn/index.tsx

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

+ 138 - 0
web/app/components/app/configuration/toolbox/annotation/config-param-modal.tsx

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

+ 124 - 0
web/app/components/app/configuration/toolbox/annotation/config-param.tsx

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

+ 4 - 0
web/app/components/app/configuration/toolbox/annotation/type.ts

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

+ 89 - 0
web/app/components/app/configuration/toolbox/annotation/use-annotation-config.ts

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

+ 19 - 1
web/app/components/app/configuration/toolbox/index.tsx

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

+ 38 - 0
web/app/components/app/configuration/toolbox/score-slider/base-slider/index.tsx

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

+ 20 - 0
web/app/components/app/configuration/toolbox/score-slider/base-slider/style.module.css

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

+ 46 - 0
web/app/components/app/configuration/toolbox/score-slider/index.tsx

@@ -0,0 +1,46 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import Slider from '@/app/components/app/configuration/toolbox/score-slider/base-slider'
+
+type Props = {
+  className?: string
+  value: number
+  onChange: (value: number) => void
+}
+
+const ScoreSlider: FC<Props> = ({
+  className,
+  value,
+  onChange,
+}) => {
+  const { t } = useTranslation()
+
+  return (
+    <div className={className}>
+      <div className='h-[1px] mt-[14px]'>
+        <Slider
+          max={100}
+          min={80}
+          step={1}
+          value={value}
+          onChange={onChange}
+        />
+      </div>
+      <div className='mt-[10px] flex justify-between items-center leading-4 text-xs font-normal '>
+        <div className='flex space-x-1 text-[#00A286]'>
+          <div>0.8</div>
+          <div>·</div>
+          <div>{t('appDebug.feature.annotation.scoreThreshold.easyMatch')}</div>
+        </div>
+        <div className='flex space-x-1 text-[#0057D8]'>
+          <div>1.0</div>
+          <div>·</div>
+          <div>{t('appDebug.feature.annotation.scoreThreshold.accurateMatch')}</div>
+        </div>
+      </div>
+    </div>
+  )
+}
+export default React.memo(ScoreSlider)

+ 45 - 0
web/app/components/app/log-annotation/index.tsx

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

+ 8 - 14
web/app/components/app/log/index.tsx

@@ -15,7 +15,7 @@ import s from './style.module.css'
 import Loading from '@/app/components/base/loading'
 import { fetchChatConversations, fetchCompletionConversations } from '@/service/log'
 import { fetchAppDetail } from '@/service/apps'
-
+import { APP_PAGE_LIMIT } from '@/config'
 export type ILogsProps = {
   appId: string
 }
@@ -26,9 +26,6 @@ export type QueryParam = {
   keyword?: string
 }
 
-// Custom page count is not currently supported.
-const limit = 10
-
 const ThreeDotsIcon = ({ className }: SVGProps<SVGElement>) => {
   return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
     <path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
@@ -60,7 +57,7 @@ const Logs: FC<ILogsProps> = ({ appId }) => {
 
   const query = {
     page: currPage + 1,
-    limit,
+    limit: APP_PAGE_LIMIT,
     ...(queryParams.period !== 'all'
       ? {
         start: dayjs().subtract(queryParams.period as number, 'day').startOf('day').format('YYYY-MM-DD HH:mm'),
@@ -93,11 +90,8 @@ const Logs: FC<ILogsProps> = ({ appId }) => {
 
   return (
     <div className='flex flex-col h-full'>
-      <div className='flex flex-col justify-center px-6 pt-4'>
-        <h1 className='flex text-xl font-medium text-gray-900'>{t('appLog.title')}</h1>
-        <p className='flex text-sm font-normal text-gray-500'>{t('appLog.description')}</p>
-      </div>
-      <div className='flex flex-col px-6 py-4 flex-1'>
+      <p className='flex text-sm font-normal text-gray-500'>{t('appLog.description')}</p>
+      <div className='flex flex-col py-4 flex-1'>
         <Filter appId={appId} queryParams={queryParams} setQueryParams={setQueryParams} />
         {total === undefined
           ? <Loading type='app' />
@@ -106,14 +100,14 @@ const Logs: FC<ILogsProps> = ({ appId }) => {
             : <EmptyElement appUrl={`${appDetail?.site.app_base_url}/${appDetail?.mode}/${appDetail?.site.access_token}`} />
         }
         {/* Show Pagination only if the total is more than the limit */}
-        {(total && total > limit)
+        {(total && total > APP_PAGE_LIMIT)
           ? <Pagination
             className="flex items-center w-full h-10 text-sm select-none mt-8"
             currentPage={currPage}
             edgePageCount={2}
             middlePagesSiblingCount={1}
             setCurrentPage={setCurrPage}
-            totalPages={Math.ceil(total / limit)}
+            totalPages={Math.ceil(total / APP_PAGE_LIMIT)}
             truncableClassName="w-8 px-0.5 text-center"
             truncableText="..."
           >
@@ -131,8 +125,8 @@ const Logs: FC<ILogsProps> = ({ appId }) => {
               />
             </div>
             <Pagination.NextButton
-              disabled={currPage === Math.ceil(total / limit) - 1}
-              className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === Math.ceil(total / limit) - 1 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} >
+              disabled={currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1}
+              className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} >
               {t('appLog.table.pagination.next')}
               <ArrowRightIcon className="ml-3 h-3 w-3" />
             </Pagination.NextButton>

+ 45 - 13
web/app/components/app/log/list.tsx

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

+ 107 - 48
web/app/components/app/text-generate/item/index.tsx

@@ -19,6 +19,8 @@ import { Bookmark } from '@/app/components/base/icons/src/vender/line/general'
 import { Stars02 } from '@/app/components/base/icons/src/vender/line/weather'
 import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
 import { fetchTextGenerationMessge } from '@/service/debug'
+import AnnotationCtrlBtn from '@/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn'
+import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
 
 const MAX_DEPTH = 3
 export type IGenerationItemProps = {
@@ -41,6 +43,10 @@ export type IGenerationItemProps = {
   installedAppId?: string
   taskId?: string
   controlClearMoreLikeThis?: number
+  supportFeedback?: boolean
+  supportAnnotation?: boolean
+  appId?: string
+  varList?: { label: string; value: string | number | object }[]
 }
 
 export const SimpleBtn = ({ className, isDisabled, onClick, children }: {
@@ -82,6 +88,10 @@ const GenerationItem: FC<IGenerationItemProps> = ({
   installedAppId,
   taskId,
   controlClearMoreLikeThis,
+  supportFeedback,
+  supportAnnotation,
+  appId,
+  varList,
 }) => {
   const { t } = useTranslation()
   const params = useParams()
@@ -100,6 +110,8 @@ const GenerationItem: FC<IGenerationItemProps> = ({
     setChildFeedback(childFeedback)
   }
 
+  const [isShowReplyModal, setIsShowReplyModal] = useState(false)
+  const question = (varList && varList?.length > 0) ? varList?.map(({ label, value }) => `${label}:${value}`).join('&') : ''
   const [isQuerying, { setTrue: startQuerying, setFalse: stopQuerying }] = useBoolean(false)
 
   const childProps = {
@@ -168,6 +180,57 @@ const GenerationItem: FC<IGenerationItemProps> = ({
     setModal(true)
   }
 
+  const ratingContent = (
+    <>
+      {!isError && messageId && !feedback?.rating && (
+        <SimpleBtn className="!px-0">
+          <>
+            <div
+              onClick={() => {
+                onFeedback?.({
+                  rating: 'like',
+                })
+              }}
+              className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
+              <HandThumbUpIcon width={16} height={16} />
+            </div>
+            <div
+              onClick={() => {
+                onFeedback?.({
+                  rating: 'dislike',
+                })
+              }}
+              className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
+              <HandThumbDownIcon width={16} height={16} />
+            </div>
+          </>
+        </SimpleBtn>
+      )}
+      {!isError && messageId && feedback?.rating === 'like' && (
+        <div
+          onClick={() => {
+            onFeedback?.({
+              rating: null,
+            })
+          }}
+          className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer  !text-primary-600 border border-primary-200 bg-primary-100 hover:border-primary-300 hover:bg-primary-200'>
+          <HandThumbUpIcon width={16} height={16} />
+        </div>
+      )}
+      {!isError && messageId && feedback?.rating === 'dislike' && (
+        <div
+          onClick={() => {
+            onFeedback?.({
+              rating: null,
+            })
+          }}
+          className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer  !text-red-600 border border-red-200 bg-red-100 hover:border-red-300 hover:bg-red-200'>
+          <HandThumbDownIcon width={16} height={16} />
+        </div>
+      )}
+    </>
+  )
+
   return (
     <div ref={ref} className={cn(className, isTop ? `rounded-xl border ${!isError ? 'border-gray-200 bg-white' : 'border-[#FECDCA] bg-[#FEF3F2]'} ` : 'rounded-br-xl !mt-0')}
       style={isTop
@@ -196,7 +259,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
                 {isError
                   ? <div className='text-gray-400 text-sm'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
                   : (
-                    <Markdown content={ content } />
+                    <Markdown content={content} />
                   )}
 
               </div>
@@ -214,7 +277,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
                         showModal => (
                           <SimpleBtn
                             isDisabled={isError || !messageId}
-                            className={cn(isMobile && '!px-1.5', 'space-x-1 mr-2')}
+                            className={cn(isMobile && '!px-1.5', 'space-x-1 mr-1')}
                             onClick={() => handleOpenLogModal(showModal)}>
                             <File02 className='w-3.5 h-3.5' />
                             {!isMobile && <div>{t('common.operation.log')}</div>}
@@ -261,54 +324,50 @@ const GenerationItem: FC<IGenerationItemProps> = ({
                       {!isMobile && <div>{t('share.generation.batchFailed.retry')}</div>}
                     </SimpleBtn>}
                     {!isError && messageId && <div className="mx-3 w-[1px] h-[14px] bg-gray-200"></div>}
-                    {!isError && messageId && !feedback?.rating && (
-                      <SimpleBtn className="!px-0">
-                        <>
-                          <div
-                            onClick={() => {
-                              onFeedback?.({
-                                rating: 'like',
-                              })
-                            }}
-                            className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
-                            <HandThumbUpIcon width={16} height={16} />
-                          </div>
-                          <div
-                            onClick={() => {
-                              onFeedback?.({
-                                rating: 'dislike',
-                              })
-                            }}
-                            className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
-                            <HandThumbDownIcon width={16} height={16} />
-                          </div>
-                        </>
-                      </SimpleBtn>
-                    )}
-                    {!isError && messageId && feedback?.rating === 'like' && (
-                      <div
-                        onClick={() => {
-                          onFeedback?.({
-                            rating: null,
-                          })
-                        }}
-                        className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer  !text-primary-600 border border-primary-200 bg-primary-100 hover:border-primary-300 hover:bg-primary-200'>
-                        <HandThumbUpIcon width={16} height={16} />
-                      </div>
-                    )}
-                    {!isError && messageId && feedback?.rating === 'dislike' && (
-                      <div
-                        onClick={() => {
-                          onFeedback?.({
-                            rating: null,
-                          })
-                        }}
-                        className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer  !text-red-600 border border-red-200 bg-red-100 hover:border-red-300 hover:bg-red-200'>
-                        <HandThumbDownIcon width={16} height={16} />
-                      </div>
-                    )}
+                    {ratingContent}
                   </>
                 )}
+
+                {supportAnnotation && (
+                  <>
+                    <div className='ml-2 mr-1 h-[14px] w-[1px] bg-gray-200'></div>
+                    <AnnotationCtrlBtn
+                      appId={appId!}
+                      messageId={messageId!}
+                      className='ml-1'
+                      query={question}
+                      answer={content}
+                      // not support cache. So can not be cached
+                      cached={false}
+                      onAdded={() => {
+
+                      }}
+                      onEdit={() => setIsShowReplyModal(true)}
+                      onRemoved={() => { }}
+                    />
+                  </>
+                )}
+
+                <EditReplyModal
+                  appId={appId!}
+                  messageId={messageId!}
+                  isShow={isShowReplyModal}
+                  onHide={() => setIsShowReplyModal(false)}
+                  query={question}
+                  answer={content}
+                  onAdded={() => { }}
+                  onEdited={() => { }}
+                  createdAt={0}
+                  onRemove={() => { }}
+                  onlyEditResponse
+                />
+
+                {supportFeedback && (
+                  <div className='ml-1'>
+                    {ratingContent}
+                  </div>
+                )
+                }
               </div>
               <div className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
             </div>

+ 69 - 0
web/app/components/base/drawer-plus/index.tsx

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

+ 3 - 1
web/app/components/base/drawer/index.tsx

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

文件差異過大導致無法顯示
+ 8 - 0
web/app/components/base/icons/assets/public/avatar/robot.svg


+ 12 - 0
web/app/components/base/icons/assets/public/avatar/user.svg

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

+ 2 - 2
web/app/components/base/icons/assets/public/billing/sparkles.svg

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

+ 5 - 0
web/app/components/base/icons/assets/vender/line/communication/message-check-remove.svg

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

+ 3 - 0
web/app/components/base/icons/assets/vender/line/communication/message-fast-plus.svg

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

+ 3 - 0
web/app/components/base/icons/assets/vender/line/files/file-download-02.svg

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

+ 3 - 0
web/app/components/base/icons/assets/vender/line/general/edit-04.svg

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

+ 3 - 0
web/app/components/base/icons/assets/vender/line/time/clock-fast-forward.svg

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

文件差異過大導致無法顯示
+ 1 - 0
web/app/components/base/icons/assets/vender/solid/communication/message-fast.svg


+ 4 - 0
web/app/components/base/icons/assets/vender/solid/general/edit-04.svg

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

文件差異過大導致無法顯示
+ 82 - 0
web/app/components/base/icons/src/public/avatar/Robot.json


+ 16 - 0
web/app/components/base/icons/src/public/avatar/Robot.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Robot.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Robot'
+
+export default Icon

+ 89 - 0
web/app/components/base/icons/src/public/avatar/User.json

@@ -0,0 +1,89 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "512",
+			"height": "512",
+			"viewBox": "0 0 512 512",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"clip-path": "url(#clip0_5968_39205)"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "rect",
+						"attributes": {
+							"width": "512",
+							"height": "512",
+							"rx": "256",
+							"fill": "#B2DDFF"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "circle",
+						"attributes": {
+							"opacity": "0.68",
+							"cx": "256",
+							"cy": "196",
+							"r": "84",
+							"fill": "white"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "ellipse",
+						"attributes": {
+							"opacity": "0.68",
+							"cx": "256",
+							"cy": "583.5",
+							"rx": "266",
+							"ry": "274.5",
+							"fill": "white"
+						},
+						"children": []
+					}
+				]
+			},
+			{
+				"type": "element",
+				"name": "defs",
+				"attributes": {},
+				"children": [
+					{
+						"type": "element",
+						"name": "clipPath",
+						"attributes": {
+							"id": "clip0_5968_39205"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "rect",
+								"attributes": {
+									"width": "512",
+									"height": "512",
+									"rx": "256",
+									"fill": "white"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "User"
+}

+ 16 - 0
web/app/components/base/icons/src/public/avatar/User.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './User.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'User'
+
+export default Icon

+ 2 - 0
web/app/components/base/icons/src/public/avatar/index.ts

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

+ 2 - 2
web/app/components/base/icons/src/public/billing/Sparkles.json

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

+ 39 - 0
web/app/components/base/icons/src/vender/line/communication/MessageCheckRemove.json

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

+ 16 - 0
web/app/components/base/icons/src/vender/line/communication/MessageCheckRemove.tsx

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

+ 29 - 0
web/app/components/base/icons/src/vender/line/communication/MessageFastPlus.json

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

+ 16 - 0
web/app/components/base/icons/src/vender/line/communication/MessageFastPlus.tsx

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

+ 2 - 0
web/app/components/base/icons/src/vender/line/communication/index.ts

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

+ 29 - 0
web/app/components/base/icons/src/vender/line/files/FileDownload02.json

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

+ 16 - 0
web/app/components/base/icons/src/vender/line/files/FileDownload02.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './FileDownload02.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'FileDownload02'
+
+export default Icon

+ 1 - 0
web/app/components/base/icons/src/vender/line/files/index.ts

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

+ 29 - 0
web/app/components/base/icons/src/vender/line/general/Edit04.json

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

+ 16 - 0
web/app/components/base/icons/src/vender/line/general/Edit04.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Edit04.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Edit04'
+
+export default Icon

+ 1 - 0
web/app/components/base/icons/src/vender/line/general/index.ts

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

+ 29 - 0
web/app/components/base/icons/src/vender/line/time/ClockFastForward.json

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

+ 16 - 0
web/app/components/base/icons/src/vender/line/time/ClockFastForward.tsx

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

+ 1 - 0
web/app/components/base/icons/src/vender/line/time/index.ts

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

文件差異過大導致無法顯示
+ 19 - 0
web/app/components/base/icons/src/vender/solid/communication/MessageFast.json


+ 16 - 0
web/app/components/base/icons/src/vender/solid/communication/MessageFast.tsx

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

+ 1 - 0
web/app/components/base/icons/src/vender/solid/communication/index.ts

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

+ 39 - 0
web/app/components/base/icons/src/vender/solid/general/Edit04.json

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

+ 16 - 0
web/app/components/base/icons/src/vender/solid/general/Edit04.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Edit04.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Edit04'
+
+export default Icon

+ 1 - 0
web/app/components/base/icons/src/vender/solid/general/index.ts

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

+ 3 - 3
web/app/components/base/markdown.tsx

@@ -81,11 +81,11 @@ const useLazyLoad = (ref: RefObject<Element>): boolean => {
   return isIntersecting
 }
 
-export function Markdown(props: { content: string }) {
+export function Markdown(props: { content: string; className?: string }) {
   const [isCopied, setIsCopied] = useState(false)
   const [isSVG, setIsSVG] = useState(false)
   return (
-    <div className="markdown-body">
+    <div className={cn(props.className, 'markdown-body')}>
       <ReactMarkdown
         remarkPlugins={[RemarkMath, RemarkGfm, RemarkBreaks]}
         rehypePlugins={[
@@ -120,7 +120,7 @@ export function Markdown(props: { content: string }) {
                       />
                     </div>
                   </div>
-                  { (language === 'mermaid' && isSVG)
+                  {(language === 'mermaid' && isSVG)
                     ? (<Flowchart PrimitiveCode={String(children).replace(/\n$/, '')} />)
                     : (<SyntaxHighlighter
                       {...props}

+ 66 - 0
web/app/components/base/modal/delete-confirm-modal/index.tsx

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

+ 16 - 0
web/app/components/base/modal/delete-confirm-modal/style.module.css

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

+ 7 - 2
web/app/components/base/modal/index.tsx

@@ -63,8 +63,13 @@ export default function Modal({
                   {description}
                 </Dialog.Description>}
                 {closable
-                  && <div className='absolute top-6 right-6 w-5 h-5 rounded-2xl flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
-                    <XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
+                  && <div className='absolute z-10 top-6 right-6 w-5 h-5 rounded-2xl flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
+                    <XMarkIcon className='w-4 h-4 text-gray-500' onClick={
+                      (e) => {
+                        e.stopPropagation()
+                        onClose()
+                      }
+                    } />
                   </div>}
                 {children}
               </Dialog.Panel>

+ 68 - 0
web/app/components/base/tab-slider-plain/index.tsx

@@ -0,0 +1,68 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import cn from 'classnames'
+
+type Option = {
+  value: string
+  text: string | JSX.Element
+}
+
+type ItemProps = {
+  className?: string
+  isActive: boolean
+  onClick: (v: string) => void
+  option: Option
+}
+const Item: FC<ItemProps> = ({
+  className,
+  isActive,
+  onClick,
+  option,
+}) => {
+  return (
+    <div
+      key={option.value}
+      className={cn(className, !isActive && 'cursor-pointer', 'relative pb-2.5  leading-6 text-base font-semibold')}
+      onClick={() => !isActive && onClick(option.value)}
+    >
+      <div className={cn(isActive ? 'text-gray-900' : 'text-gray-600')}>{option.text}</div>
+      {isActive && (
+        <div className='absolute bottom-0 left-0 right-0 h-0.5 bg-[#155EEF]'></div>
+      )}
+    </div>
+  )
+}
+
+type Props = {
+  className?: string
+  value: string
+  onChange: (v: string) => void
+  options: Option[]
+  noBorderBottom?: boolean
+  itemClassName?: string
+}
+
+const TabSlider: FC<Props> = ({
+  className,
+  value,
+  onChange,
+  options,
+  noBorderBottom,
+  itemClassName,
+}) => {
+  return (
+    <div className={cn(className, !noBorderBottom && 'border-b border-[#EAECF0]', 'flex  space-x-6')}>
+      {options.map(option => (
+        <Item
+          isActive={option.value === value}
+          option={option}
+          onClick={onChange}
+          key={option.value}
+          className={itemClassName}
+        />
+      ))}
+    </div>
+  )
+}
+export default React.memo(TabSlider)

+ 31 - 0
web/app/components/billing/annotation-full/index.tsx

@@ -0,0 +1,31 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import UpgradeBtn from '../upgrade-btn'
+import Usage from './usage'
+import s from './style.module.css'
+import GridMask from '@/app/components/base/grid-mask'
+
+const AnnotationFull: FC = () => {
+  const { t } = useTranslation()
+
+  return (
+    <GridMask wrapperClassName='rounded-lg' canvasClassName='rounded-lg' gradientClassName='rounded-lg'>
+      <div className='mt-6 px-3.5 py-4 border-2 border-solid border-transparent rounded-lg shadow-md flex flex-col transition-all duration-200 ease-in-out cursor-pointer'>
+        <div className='flex justify-between items-center'>
+          <div className={cn(s.textGradient, 'leading-[24px] text-base font-semibold')}>
+            <div>{t('billing.annotatedResponse.fullTipLine1')}</div>
+            <div>{t('billing.annotatedResponse.fullTipLine2')}</div>
+          </div>
+          <div className='flex'>
+            <UpgradeBtn loc={'annotation-create'} />
+          </div>
+        </div>
+        <Usage className='mt-4' />
+      </div>
+    </GridMask>
+  )
+}
+export default React.memo(AnnotationFull)

+ 47 - 0
web/app/components/billing/annotation-full/modal.tsx

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

+ 7 - 0
web/app/components/billing/annotation-full/style.module.css

@@ -0,0 +1,7 @@
+.textGradient {
+  background: linear-gradient(92deg, #2250F2 -29.55%, #0EBCF3 75.22%);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+  text-fill-color: transparent;
+}

+ 32 - 0
web/app/components/billing/annotation-full/usage.tsx

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

+ 2 - 0
web/app/components/billing/config.ts

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

+ 1 - 1
web/app/components/billing/progress-bar/index.tsx

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

+ 5 - 1
web/app/components/billing/type.ts

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

+ 2 - 0
web/app/components/billing/utils/index.ts

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

+ 358 - 0
web/app/components/header/account-setting/model-page/model-selector/portal-select.tsx

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

+ 16 - 0
web/app/components/share/chat/index.tsx

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

+ 1 - 0
web/app/components/share/text-generation/result/index.tsx

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

部分文件因文件數量過多而無法顯示