Browse Source

Feat/chat add origin (#1130)

zxhlyh 1 year ago
parent
commit
84c76bc04a
74 changed files with 2454 additions and 28 deletions
  1. 12 3
      web/app/components/app/chat/answer/index.tsx
  2. 123 0
      web/app/components/app/chat/citation/index.tsx
  3. 123 0
      web/app/components/app/chat/citation/popup.tsx
  4. 46 0
      web/app/components/app/chat/citation/progress-tooltip.tsx
  5. 46 0
      web/app/components/app/chat/citation/tooltip.tsx
  6. 8 0
      web/app/components/app/chat/index.tsx
  7. 21 0
      web/app/components/app/chat/type.ts
  8. 0 0
      web/app/components/app/configuration/base/icons/citation.tsx
  9. 150 0
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/citation.svg
  10. 4 0
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/style.module.css
  11. 10 0
      web/app/components/app/configuration/config/feature/choose-feature/index.tsx
  12. 8 0
      web/app/components/app/configuration/config/feature/use-feature.tsx
  13. 10 1
      web/app/components/app/configuration/config/index.tsx
  14. 19 1
      web/app/components/app/configuration/debug/index.tsx
  15. 25 0
      web/app/components/app/configuration/features/chat-group/citation/index.tsx
  16. 8 0
      web/app/components/app/configuration/features/chat-group/index.tsx
  17. 15 0
      web/app/components/app/configuration/index.tsx
  18. 48 0
      web/app/components/base/file-icon/index.tsx
  19. 12 0
      web/app/components/base/icons/assets/public/common/notion.svg
  20. 23 0
      web/app/components/base/icons/assets/public/files/html.svg
  21. 23 0
      web/app/components/base/icons/assets/public/files/json.svg
  22. 22 0
      web/app/components/base/icons/assets/public/files/pdf.svg
  23. 23 0
      web/app/components/base/icons/assets/public/files/txt.svg
  24. 23 0
      web/app/components/base/icons/assets/public/files/unknow.svg
  25. 18 0
      web/app/components/base/icons/assets/public/files/xlsx.svg
  26. 5 0
      web/app/components/base/icons/assets/vender/line/editor/bezier-curve-03.svg
  27. 5 0
      web/app/components/base/icons/assets/vender/line/editor/type-square.svg
  28. 10 0
      web/app/components/base/icons/assets/vender/line/general/target-04.svg
  29. 5 0
      web/app/components/base/icons/assets/vender/solid/editor/citations.svg
  30. 83 0
      web/app/components/base/icons/src/public/common/Notion.json
  31. 16 0
      web/app/components/base/icons/src/public/common/Notion.tsx
  32. 1 0
      web/app/components/base/icons/src/public/common/index.ts
  33. 178 0
      web/app/components/base/icons/src/public/files/Html.json
  34. 16 0
      web/app/components/base/icons/src/public/files/Html.tsx
  35. 178 0
      web/app/components/base/icons/src/public/files/Json.json
  36. 16 0
      web/app/components/base/icons/src/public/files/Json.tsx
  37. 169 0
      web/app/components/base/icons/src/public/files/Pdf.json
  38. 16 0
      web/app/components/base/icons/src/public/files/Pdf.tsx
  39. 180 0
      web/app/components/base/icons/src/public/files/Txt.json
  40. 16 0
      web/app/components/base/icons/src/public/files/Txt.tsx
  41. 199 0
      web/app/components/base/icons/src/public/files/Unknow.json
  42. 16 0
      web/app/components/base/icons/src/public/files/Unknow.tsx
  43. 145 0
      web/app/components/base/icons/src/public/files/Xlsx.json
  44. 16 0
      web/app/components/base/icons/src/public/files/Xlsx.tsx
  45. 6 0
      web/app/components/base/icons/src/public/files/index.ts
  46. 2 0
      web/app/components/base/icons/src/public/llm/Localai.tsx
  47. 2 0
      web/app/components/base/icons/src/public/llm/LocalaiText.tsx
  48. 38 0
      web/app/components/base/icons/src/vender/line/editor/BezierCurve03.json
  49. 16 0
      web/app/components/base/icons/src/vender/line/editor/BezierCurve03.tsx
  50. 38 0
      web/app/components/base/icons/src/vender/line/editor/TypeSquare.json
  51. 16 0
      web/app/components/base/icons/src/vender/line/editor/TypeSquare.tsx
  52. 2 0
      web/app/components/base/icons/src/vender/line/editor/index.ts
  53. 65 0
      web/app/components/base/icons/src/vender/line/general/Target04.json
  54. 16 0
      web/app/components/base/icons/src/vender/line/general/Target04.tsx
  55. 1 0
      web/app/components/base/icons/src/vender/line/general/index.ts
  56. 36 0
      web/app/components/base/icons/src/vender/solid/editor/Citations.json
  57. 16 0
      web/app/components/base/icons/src/vender/solid/editor/Citations.tsx
  58. 1 0
      web/app/components/base/icons/src/vender/solid/editor/index.ts
  59. 2 2
      web/app/components/base/portal-to-follow-elem/index.tsx
  60. 1 1
      web/app/components/explore/item-operation/index.tsx
  61. 18 1
      web/app/components/explore/universal-chat/index.tsx
  62. 23 2
      web/app/components/share/chat/index.tsx
  63. 3 2
      web/app/components/share/chatbot/index.tsx
  64. 7 1
      web/context/debug-configuration.ts
  65. 5 0
      web/i18n/lang/app-debug.en.ts
  66. 5 0
      web/i18n/lang/app-debug.zh.ts
  67. 8 0
      web/i18n/lang/common.en.ts
  68. 8 0
      web/i18n/lang/common.zh.ts
  69. 5 0
      web/models/debug.ts
  70. 9 5
      web/service/base.ts
  71. 4 3
      web/service/debug.ts
  72. 4 3
      web/service/share.ts
  73. 4 3
      web/service/universal-chat.ts
  74. 3 0
      web/types/app.ts

+ 12 - 3
web/app/components/app/chat/answer/index.tsx

@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'
 import { useContext } from 'use-context-selector'
 import { UserCircleIcon } from '@heroicons/react/24/solid'
 import cn from 'classnames'
-import type { DisplayScene, FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc, ThoughtItem } from '../type'
+import type { CitationItem, DisplayScene, FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc, ThoughtItem } from '../type'
 import OperationBtn from '../operation'
 import LoadingAnim from '../loading-anim'
 import { EditIcon, EditIconSolid, OpeningStatementIcon, RatingIcon } from '../icon-component'
@@ -13,6 +13,7 @@ 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'
@@ -45,11 +46,14 @@ export type IAnswerProps = {
   isResponsing?: boolean
   answerIconClassName?: string
   thoughts?: ThoughtItem[]
+  citation?: CitationItem[]
   isThinking?: boolean
   dataSets?: DataSet[]
+  isShowCitation?: boolean
+  isShowCitationHitInfo?: boolean
 }
 // The component needs to maintain its own state to control whether to display input component
-const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedbackEdit = false, onFeedback, onSubmitAnnotation, displayScene = 'web', isResponsing, answerIconClassName, thoughts, isThinking, dataSets }) => {
+const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedbackEdit = false, onFeedback, onSubmitAnnotation, displayScene = 'web', isResponsing, answerIconClassName, thoughts, citation, isThinking, dataSets, isShowCitation, isShowCitationHitInfo = false }) => {
   const { id, content, more, feedback, adminFeedback, annotation: initAnnotation } = item
   const [showEdit, setShowEdit] = useState(false)
   const [loading, setLoading] = useState(false)
@@ -171,7 +175,7 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedba
             </div>
           }
         </div>
-        <div className={s.answerWrapWrap}>
+        <div className={cn(s.answerWrapWrap, 'chat-answer-container')}>
           <div className={`${s.answerWrap} ${showEdit ? 'w-full' : ''}`}>
             <div className={`${s.answer} relative text-sm text-gray-900`}>
               <div className={'ml-2 py-3 px-4 bg-gray-100 rounded-tr-2xl rounded-b-2xl'}>
@@ -237,6 +241,11 @@ const Answer: FC<IAnswerProps> = ({ item, feedbackDisabled = false, isHideFeedba
                     </div>
                   </>
                 }
+                {
+                  !!citation?.length && !isThinking && isShowCitation && !isResponsing && (
+                    <Citation data={citation} showHitInfo={isShowCitationHitInfo} />
+                  )
+                }
               </div>
               <div className='absolute top-[-14px] right-[-14px] flex flex-row justify-end gap-1'>
                 {!item.isOpeningStatement && (

+ 123 - 0
web/app/components/app/chat/citation/index.tsx

@@ -0,0 +1,123 @@
+import { useEffect, useMemo, useRef, useState } from 'react'
+import type { FC } from 'react'
+import { useTranslation } from 'react-i18next'
+import type { CitationItem } from '../type'
+import Popup from './popup'
+import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
+
+export type Resources = {
+  documentId: string
+  documentName: string
+  dataSourceType: string
+  sources: CitationItem[]
+}
+
+type CitationProps = {
+  data: CitationItem[]
+  showHitInfo?: boolean
+}
+const Citation: FC<CitationProps> = ({
+  data,
+  showHitInfo,
+}) => {
+  const { t } = useTranslation()
+  const elesRef = useRef<HTMLDivElement[]>([])
+  const [limitNumberInOneLine, setlimitNumberInOneLine] = useState(0)
+  const [showMore, setShowMore] = useState(false)
+  const resources = useMemo(() => data.reduce((prev: Resources[], next) => {
+    const documentId = next.document_id
+    const documentName = next.document_name
+    const dataSourceType = next.data_source_type
+    const documentIndex = prev.findIndex(i => i.documentId === documentId)
+
+    if (documentIndex > -1) {
+      prev[documentIndex].sources.push(next)
+    }
+    else {
+      prev.push({
+        documentId,
+        documentName,
+        dataSourceType,
+        sources: [next],
+      })
+    }
+
+    return prev
+  }, []), [data])
+
+  const handleAdjustResourcesLayout = () => {
+    const containerWidth = document.querySelector('.chat-answer-container')!.clientWidth - 40
+    let totalWidth = 0
+    for (let i = 0; i < resources.length; i++) {
+      totalWidth += elesRef.current[i].clientWidth
+
+      if (totalWidth + i * 4 > containerWidth!) {
+        totalWidth -= elesRef.current[i].clientWidth
+
+        if (totalWidth + 34 > containerWidth!)
+          setlimitNumberInOneLine(i - 1)
+        else
+          setlimitNumberInOneLine(i)
+
+        break
+      }
+      else {
+        setlimitNumberInOneLine(i + 1)
+      }
+    }
+  }
+
+  useEffect(() => {
+    handleAdjustResourcesLayout()
+  }, [])
+
+  const resourcesLength = resources.length
+
+  return (
+    <div className='mt-3 -mb-1'>
+      <div className='flex items-center mb-2 text-xs font-medium text-gray-500'>
+        {t('common.chat.citation.title')}
+        <div className='grow ml-2 h-[1px] bg-black/5' />
+      </div>
+      <div className='relative flex flex-wrap'>
+        {
+          resources.map((res, index) => (
+            <div
+              key={index}
+              className='absolute top-0 left-0 w-auto mr-1 mb-1 pl-7 pr-2 max-w-[240px] h-7 text-xs whitespace-nowrap opacity-0 -z-10'
+              ref={ele => (elesRef.current[index] = ele!)}
+            >
+              {res.documentName}
+            </div>
+          ))
+        }
+        {
+          resources.slice(0, showMore ? resourcesLength : limitNumberInOneLine).map((res, index) => (
+            <div key={index} className='mr-1 mb-1 cursor-pointer'>
+              <Popup
+                data={res}
+                showHitInfo={showHitInfo}
+              />
+            </div>
+          ))
+        }
+        {
+          limitNumberInOneLine < resourcesLength && (
+            <div
+              className='flex items-center px-2 h-7 bg-white rounded-lg text-xs font-medium text-gray-500 cursor-pointer'
+              onClick={() => setShowMore(v => !v)}
+            >
+              {
+                !showMore
+                  ? `+ ${resourcesLength - limitNumberInOneLine}`
+                  : <ChevronDown className='w-4 h-4 text-gray-600 rotate-180' />
+              }
+            </div>
+          )
+        }
+      </div>
+    </div>
+  )
+}
+
+export default Citation

+ 123 - 0
web/app/components/app/chat/citation/popup.tsx

@@ -0,0 +1,123 @@
+import { Fragment, useState } from 'react'
+import type { FC } from 'react'
+import Link from 'next/link'
+import { useTranslation } from 'react-i18next'
+import Tooltip from './tooltip'
+import ProgressTooltip from './progress-tooltip'
+import type { Resources } from './index'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+import FileIcon from '@/app/components/base/file-icon'
+import {
+  Hash02,
+  Target04,
+} from '@/app/components/base/icons/src/vender/line/general'
+import { ArrowUpRight } from '@/app/components/base/icons/src/vender/line/arrows'
+import {
+  BezierCurve03,
+  TypeSquare,
+} from '@/app/components/base/icons/src/vender/line/editor'
+
+type PopupProps = {
+  data: Resources
+  showHitInfo?: boolean
+}
+
+const Popup: FC<PopupProps> = ({
+  data,
+  showHitInfo = false,
+}) => {
+  const { t } = useTranslation()
+  const [open, setOpen] = useState(false)
+  const fileType = data.dataSourceType === 'upload_file'
+    ? (/\.([^.]*)$/g.exec(data.documentName)?.[1] || '')
+    : 'notion'
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement='top-start'
+      offset={{
+        mainAxis: 8,
+        crossAxis: -2,
+      }}
+    >
+      <PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
+        <div className='flex items-center px-2 max-w-[240px] h-7 bg-white rounded-lg'>
+          <FileIcon type={fileType} className='mr-1 w-4 h-4' />
+          <div className='text-xs text-gray-600 truncate'>{data.documentName}</div>
+        </div>
+      </PortalToFollowElemTrigger>
+      <PortalToFollowElemContent style={{ zIndex: 1000 }}>
+        <div className='w-[360px] bg-gray-50 rounded-xl shadow-lg'>
+          <div className='px-4 pt-3 pb-2'>
+            <div className='flex items-center h-[18px]'>
+              <FileIcon type={fileType} className='mr-1 w-4 h-4' />
+              <div className='text-xs font-medium text-gray-600 truncate'>{data.documentName}</div>
+            </div>
+          </div>
+          <div className='px-4 py-0.5 max-h-[450px] bg-white rounded-lg overflow-auto'>
+            {
+              data.sources.map((source, index) => (
+                <Fragment key={index}>
+                  <div className='group py-3'>
+                    {
+                      showHitInfo && (
+                        <div className='flex items-center justify-between mb-2'>
+                          <div className='flex items-center px-1.5 h-5 border border-gray-200 rounded-md'>
+                            <Hash02 className='mr-0.5 w-3 h-3 text-gray-400' />
+                            <div className='text-[11px] font-medium text-gray-500'>{source.segment_position}</div>
+                          </div>
+                          <Link
+                            href={`/datasets/${source.dataset_id}/documents/${source.document_id}`}
+                            className='hidden items-center h-[18px] text-xs text-primary-600 group-hover:flex'>
+                            Link to dataset
+                            <ArrowUpRight className='ml-1 w-3 h-3' />
+                          </Link>
+                        </div>
+                      )
+                    }
+                    <div className='text-[13px] text-gray-800'>{source.content}</div>
+                    {
+                      showHitInfo && (
+                        <div className='flex items-center mt-2 text-xs font-medium text-gray-500'>
+                          <Tooltip
+                            text={t('common.chat.citation.characters')}
+                            data={source.word_count}
+                            icon={<TypeSquare className='mr-1 w-3 h-3' />}
+                          />
+                          <Tooltip
+                            text={t('common.chat.citation.hitCount')}
+                            data={source.hit_count}
+                            icon={<Target04 className='mr-1 w-3 h-3' />}
+                          />
+                          <Tooltip
+                            text={t('common.chat.citation.vectorHash')}
+                            data={source.index_node_hash.substring(0, 7)}
+                            icon={<BezierCurve03 className='mr-1 w-3 h-3' />}
+                          />
+                          <ProgressTooltip data={Number(source.score.toFixed(2))} />
+                        </div>
+                      )
+                    }
+                  </div>
+                  {
+                    index !== data.sources.length - 1 && (
+                      <div className='my-1 h-[1px] bg-black/5' />
+                    )
+                  }
+                </Fragment>
+              ))
+            }
+          </div>
+        </div>
+      </PortalToFollowElemContent>
+    </PortalToFollowElem>
+  )
+}
+
+export default Popup

+ 46 - 0
web/app/components/app/chat/citation/progress-tooltip.tsx

@@ -0,0 +1,46 @@
+import { useState } from 'react'
+import type { FC } from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+
+type ProgressTooltipProps = {
+  data: number
+}
+
+const ProgressTooltip: FC<ProgressTooltipProps> = ({
+  data,
+}) => {
+  const { t } = useTranslation()
+  const [open, setOpen] = useState(false)
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement='top-start'
+    >
+      <PortalToFollowElemTrigger
+        onMouseEnter={() => setOpen(true)}
+        onMouseLeave={() => setOpen(false)}
+      >
+        <div className='grow flex items-center'>
+          <div className='mr-1 w-16 h-1.5 rounded-[3px] border border-gray-400 overflow-hidden'>
+            <div className='bg-gray-400 h-full' style={{ width: `${data * 100}%` }}></div>
+          </div>
+          {data}
+        </div>
+      </PortalToFollowElemTrigger>
+      <PortalToFollowElemContent style={{ zIndex: 1001 }}>
+        <div className='p-3 bg-white text-xs font-medium text-gray-500 rounded-lg shadow-lg'>
+          {t('common.chat.citation.hitScore')} {data}
+        </div>
+      </PortalToFollowElemContent>
+    </PortalToFollowElem>
+  )
+}
+
+export default ProgressTooltip

+ 46 - 0
web/app/components/app/chat/citation/tooltip.tsx

@@ -0,0 +1,46 @@
+import React, { useState } from 'react'
+import type { FC } from 'react'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+
+type TooltipProps = {
+  data: number | string
+  text: string
+  icon: React.ReactNode
+}
+
+const Tooltip: FC<TooltipProps> = ({
+  data,
+  text,
+  icon,
+}) => {
+  const [open, setOpen] = useState(false)
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement='top-start'
+    >
+      <PortalToFollowElemTrigger
+        onMouseEnter={() => setOpen(true)}
+        onMouseLeave={() => setOpen(false)}
+      >
+        <div className='flex items-center mr-6'>
+          {icon}
+          {data}
+        </div>
+      </PortalToFollowElemTrigger>
+      <PortalToFollowElemContent style={{ zIndex: 1001 }}>
+        <div className='p-3 bg-white text-xs font-medium text-gray-500 rounded-lg shadow-lg'>
+          {text} {data}
+        </div>
+      </PortalToFollowElemContent>
+    </PortalToFollowElem>
+  )
+}
+
+export default Tooltip

+ 8 - 0
web/app/components/app/chat/index.tsx

@@ -48,9 +48,11 @@ export type IChatProps = {
   isShowSuggestion?: boolean
   suggestionList?: string[]
   isShowSpeechToText?: boolean
+  isShowCitation?: boolean
   answerIconClassName?: string
   isShowConfigElem?: boolean
   dataSets?: DataSet[]
+  isShowCitationHitInfo?: boolean
 }
 
 const Chat: FC<IChatProps> = ({
@@ -74,9 +76,11 @@ const Chat: FC<IChatProps> = ({
   isShowSuggestion,
   suggestionList,
   isShowSpeechToText,
+  isShowCitation,
   answerIconClassName,
   isShowConfigElem,
   dataSets,
+  isShowCitationHitInfo,
 }) => {
   const { t } = useTranslation()
   const { notify } = useContext(ToastContext)
@@ -162,6 +166,7 @@ const Chat: FC<IChatProps> = ({
           if (item.isAnswer) {
             const isLast = item.id === chatList[chatList.length - 1].id
             const thoughts = item.agent_thoughts?.filter(item => item.thought !== '[DONE]')
+            const citation = item.citation
             const isThinking = !item.content && item.agent_thoughts && item.agent_thoughts?.length > 0 && !item.agent_thoughts.some(item => item.thought === '[DONE]')
             return <Answer
               key={item.id}
@@ -174,8 +179,11 @@ const Chat: FC<IChatProps> = ({
               isResponsing={isResponsing && isLast}
               answerIconClassName={answerIconClassName}
               thoughts={thoughts}
+              citation={citation}
               isThinking={isThinking}
               dataSets={dataSets}
+              isShowCitation={isShowCitation}
+              isShowCitationHitInfo={isShowCitationHitInfo}
             />
           }
           return <Question key={item.id} id={item.id} content={item.content} more={item.more} useCurrentUserAvatar={useCurrentUserAvatar} />

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

@@ -23,10 +23,26 @@ export type ThoughtItem = {
   tool_input: string
   message_id: string
 }
+export type CitationItem = {
+  content: string
+  data_source_type: string
+  dataset_name: string
+  dataset_id: string
+  document_id: string
+  document_name: string
+  hit_count: number
+  index_node_hash: string
+  segment_id: string
+  segment_position: number
+  score: number
+  word_count: number
+}
+
 export type IChatItem = {
   id: string
   content: string
   agent_thoughts?: ThoughtItem[]
+  citation?: CitationItem[]
   /**
    * Specific message type
    */
@@ -51,3 +67,8 @@ export type IChatItem = {
   useCurrentUserAvatar?: boolean
   isOpeningStatement?: boolean
 }
+
+export type MessageEnd = {
+  id: string
+  retriever_resources?: CitationItem[]
+}

+ 0 - 0
web/app/components/app/configuration/base/icons/citation.tsx


File diff suppressed because it is too large
+ 150 - 0
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/citation.svg


+ 4 - 0
web/app/components/app/configuration/config/feature/choose-feature/feature-item/style.module.css

@@ -26,4 +26,8 @@
 
 .speechToTextPreview {
   background-image: url(./preview-imgs/speech-to-text.svg);
+}
+
+.citationPreview {
+  background-image: url(./preview-imgs/citation.svg);
 }

+ 10 - 0
web/app/components/app/configuration/config/feature/choose-feature/index.tsx

@@ -8,11 +8,13 @@ import FeatureItem from './feature-item'
 import Modal from '@/app/components/base/modal'
 import SuggestedQuestionsAfterAnswerIcon from '@/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon'
 import { Microphone01 } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
+import { Citations } from '@/app/components/base/icons/src/vender/solid/editor'
 type IConfig = {
   openingStatement: boolean
   moreLikeThis: boolean
   suggestedQuestionsAfterAnswer: boolean
   speechToText: boolean
+  citation: boolean
 }
 
 export type IChooseFeatureProps = {
@@ -85,6 +87,14 @@ const ChooseFeature: FC<IChooseFeatureProps> = ({
                   />
                 )
               }
+              <FeatureItem
+                icon={<Citations className='w-4 h-4 text-[#FD853A]' />}
+                previewImgClassName='citationPreview'
+                title={t('appDebug.feature.citation.title')}
+                description={t('appDebug.feature.citation.description')}
+                value={config.citation}
+                onChange={value => onChange('citation', value)}
+              />
             </>
           </FeatureGroup>
         )}

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

@@ -9,6 +9,8 @@ function useFeature({
   setSuggestedQuestionsAfterAnswer,
   speechToText,
   setSpeechToText,
+  citation,
+  setCitation,
 }: {
   introduction: string
   setIntroduction: (introduction: string) => void
@@ -18,6 +20,8 @@ function useFeature({
   setSuggestedQuestionsAfterAnswer: (suggestedQuestionsAfterAnswer: boolean) => void
   speechToText: boolean
   setSpeechToText: (speechToText: boolean) => void
+  citation: boolean
+  setCitation: (citation: boolean) => void
 }) {
   const [tempshowOpeningStatement, setTempShowOpeningStatement] = React.useState(!!introduction)
   useEffect(() => {
@@ -36,6 +40,7 @@ function useFeature({
     moreLikeThis,
     suggestedQuestionsAfterAnswer,
     speechToText,
+    citation,
   }
   const handleFeatureChange = (key: string, value: boolean) => {
     switch (key) {
@@ -53,6 +58,9 @@ function useFeature({
         break
       case 'speechToText':
         setSpeechToText(value)
+        break
+      case 'citation':
+        setCitation(value)
     }
   }
   return {

+ 10 - 1
web/app/components/app/configuration/config/index.tsx

@@ -36,6 +36,8 @@ const Config: FC = () => {
     setSuggestedQuestionsAfterAnswerConfig,
     speechToTextConfig,
     setSpeechToTextConfig,
+    citationConfig,
+    setCitationConfig,
   } = useContext(ConfigContext)
   const isChatApp = mode === AppType.chat
   const { speech2textDefaultModel } = useProviderContext()
@@ -88,9 +90,15 @@ const Config: FC = () => {
         draft.enabled = value
       }))
     },
+    citation: citationConfig.enabled,
+    setCitation: (value) => {
+      setCitationConfig(produce(citationConfig, (draft) => {
+        draft.enabled = value
+      }))
+    },
   })
 
-  const hasChatConfig = isChatApp && (featureConfig.openingStatement || featureConfig.suggestedQuestionsAfterAnswer || (featureConfig.speechToText && !!speech2textDefaultModel))
+  const hasChatConfig = isChatApp && (featureConfig.openingStatement || featureConfig.suggestedQuestionsAfterAnswer || (featureConfig.speechToText && !!speech2textDefaultModel) || featureConfig.citation)
   const hasToolbox = false
 
   const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
@@ -161,6 +169,7 @@ const Config: FC = () => {
               }
               isShowSuggestedQuestionsAfterAnswer={featureConfig.suggestedQuestionsAfterAnswer}
               isShowSpeechText={featureConfig.speechToText && !!speech2textDefaultModel}
+              isShowCitation={featureConfig.citation}
             />
           )
         }

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

@@ -40,6 +40,7 @@ const Debug: FC<IDebug> = ({
     introduction,
     suggestedQuestionsAfterAnswerConfig,
     speechToTextConfig,
+    citationConfig,
     moreLikeThisConfig,
     inputs,
     // setInputs,
@@ -162,6 +163,7 @@ const Debug: FC<IDebug> = ({
       },
       suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
       speech_to_text: speechToTextConfig,
+      retriever_resource: citationConfig,
       agent_mode: {
         enabled: true,
         tools: [...postDatasets],
@@ -199,7 +201,7 @@ const Debug: FC<IDebug> = ({
     setChatList(newList)
 
     // answer
-    const responseItem = {
+    const responseItem: IChatItem = {
       id: `${Date.now()}`,
       content: '',
       isAnswer: true,
@@ -266,6 +268,19 @@ const Debug: FC<IDebug> = ({
           setIsShowSuggestion(true)
         }
       },
+      onMessageEnd: (messageEnd) => {
+        responseItem.citation = messageEnd.retriever_resources
+
+        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 })
+          })
+        setChatList(newListWithAnswer)
+      },
       onError() {
         setResponsingFalse()
         // role back placeholder answer
@@ -312,6 +327,7 @@ const Debug: FC<IDebug> = ({
       opening_statement: introduction,
       suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
       speech_to_text: speechToTextConfig,
+      retriever_resource: citationConfig,
       more_like_this: moreLikeThisConfig,
       agent_mode: {
         enabled: true,
@@ -391,6 +407,8 @@ const Debug: FC<IDebug> = ({
                   isShowSuggestion={doShowSuggestion}
                   suggestionList={suggestQuestions}
                   isShowSpeechToText={speechToTextConfig.enabled && !!speech2textDefaultModel}
+                  isShowCitation={citationConfig.enabled}
+                  isShowCitationHitInfo
                 />
               </div>
             </div>

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

@@ -0,0 +1,25 @@
+'use client'
+import React, { type FC } from 'react'
+import { useTranslation } from 'react-i18next'
+import Panel from '@/app/components/app/configuration/base/feature-panel'
+import { Citations } from '@/app/components/base/icons/src/vender/solid/editor'
+
+const Citation: FC = () => {
+  const { t } = useTranslation()
+
+  return (
+    <Panel
+      title={
+        <div className='flex items-center gap-2'>
+          <div>{t('appDebug.feature.citation.title')}</div>
+        </div>
+      }
+      headerIcon={<Citations className='w-4 h-4 text-[#FD853A]' />}
+      headerRight={
+        <div className='text-xs text-gray-500'>{t('appDebug.feature.citation.resDes')}</div>
+      }
+      noBodySpacing
+    />
+  )
+}
+export default React.memo(Citation)

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

@@ -7,6 +7,7 @@ import type { IOpeningStatementProps } from './opening-statement'
 import OpeningStatement from './opening-statement'
 import SuggestedQuestionsAfterAnswer from './suggested-questions-after-answer'
 import SpeechToText from './speech-to-text'
+import Citation from './citation'
 
 /*
 * Include
@@ -19,12 +20,14 @@ type ChatGroupProps = {
   openingStatementConfig: IOpeningStatementProps
   isShowSuggestedQuestionsAfterAnswer: boolean
   isShowSpeechText: boolean
+  isShowCitation: boolean
 }
 const ChatGroup: FC<ChatGroupProps> = ({
   isShowOpeningStatement,
   openingStatementConfig,
   isShowSuggestedQuestionsAfterAnswer,
   isShowSpeechText,
+  isShowCitation,
 }) => {
   const { t } = useTranslation()
 
@@ -43,6 +46,11 @@ const ChatGroup: FC<ChatGroupProps> = ({
             <SpeechToText />
           )
         }
+        {
+          isShowCitation && (
+            <Citation />
+          )
+        }
       </div>
     </div>
   )

+ 15 - 0
web/app/components/app/configuration/index.tsx

@@ -56,6 +56,9 @@ const Configuration: FC = () => {
   const [speechToTextConfig, setSpeechToTextConfig] = useState<MoreLikeThisConfig>({
     enabled: false,
   })
+  const [citationConfig, setCitationConfig] = useState<MoreLikeThisConfig>({
+    enabled: false,
+  })
   const [formattingChanged, setFormattingChanged] = useState(false)
   const [inputs, setInputs] = useState<Inputs>({})
   const [query, setQuery] = useState('')
@@ -77,6 +80,7 @@ const Configuration: FC = () => {
     more_like_this: null,
     suggested_questions_after_answer: null,
     speech_to_text: null,
+    retriever_resource: null,
     dataSets: [],
   })
 
@@ -110,6 +114,9 @@ const Configuration: FC = () => {
     setSpeechToTextConfig(modelConfig.speech_to_text || {
       enabled: false,
     })
+    setCitationConfig(modelConfig.retriever_resource || {
+      enabled: false,
+    })
   }
 
   const { textGenerationModelList } = useProviderContext()
@@ -159,6 +166,9 @@ const Configuration: FC = () => {
       if (modelConfig.speech_to_text)
         setSpeechToTextConfig(modelConfig.speech_to_text)
 
+      if (modelConfig.retriever_resource)
+        setCitationConfig(modelConfig.retriever_resource)
+
       const config = {
         modelConfig: {
           provider: model.provider,
@@ -171,6 +181,7 @@ const Configuration: FC = () => {
           more_like_this: modelConfig.more_like_this,
           suggested_questions_after_answer: modelConfig.suggested_questions_after_answer,
           speech_to_text: modelConfig.speech_to_text,
+          retriever_resource: modelConfig.retriever_resource,
           dataSets: datasets || [],
         },
         completionParams: model.completion_params,
@@ -202,6 +213,7 @@ const Configuration: FC = () => {
       more_like_this: moreLikeThisConfig,
       suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
       speech_to_text: speechToTextConfig,
+      retriever_resource: citationConfig,
       agent_mode: {
         enabled: true,
         tools: [...postDatasets],
@@ -219,6 +231,7 @@ const Configuration: FC = () => {
       draft.more_like_this = moreLikeThisConfig
       draft.suggested_questions_after_answer = suggestedQuestionsAfterAnswerConfig
       draft.speech_to_text = speechToTextConfig
+      draft.retriever_resource = citationConfig
       draft.dataSets = dataSets
     })
     setPublishedConfig({
@@ -263,6 +276,8 @@ const Configuration: FC = () => {
       setSuggestedQuestionsAfterAnswerConfig,
       speechToTextConfig,
       setSpeechToTextConfig,
+      citationConfig,
+      setCitationConfig,
       formattingChanged,
       setFormattingChanged,
       inputs,

+ 48 - 0
web/app/components/base/file-icon/index.tsx

@@ -0,0 +1,48 @@
+import type { FC } from 'react'
+import {
+  Csv,
+  Html,
+  Json,
+  Md,
+  Pdf,
+  Txt,
+  Unknow,
+  Xlsx,
+} from '@/app/components/base/icons/src/public/files'
+import { Notion } from '@/app/components/base/icons/src/public/common'
+
+type FileIconProps = {
+  type: string
+  className?: string
+}
+
+const FileIcon: FC<FileIconProps> = ({
+  type,
+  className,
+}) => {
+  switch (type) {
+    case 'csv':
+      return <Csv className={className} />
+    case 'htm':
+    case 'html':
+      return <Html className={className} />
+    case 'json':
+      return <Json className={className} />
+    case 'md':
+    case 'markdown':
+      return <Md className={className} />
+    case 'pdf':
+      return <Pdf className={className} />
+    case 'txt':
+      return <Txt className={className} />
+    case 'xls':
+    case 'xlsx':
+      return <Xlsx className={className} />
+    case 'notion':
+      return <Notion className={className} />
+    default:
+      return <Unknow className={className} />
+  }
+}
+
+export default FileIcon

File diff suppressed because it is too large
+ 12 - 0
web/app/components/base/icons/assets/public/common/notion.svg


+ 23 - 0
web/app/components/base/icons/assets/public/files/html.svg

@@ -0,0 +1,23 @@
+<svg width="32" height="34" viewBox="0 0 32 34" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g filter="url(#filter0_d_3055_14424)">
+<path d="M4 7.73349C4 5.49329 4 4.37318 4.43597 3.51753C4.81947 2.76489 5.43139 2.15296 6.18404 1.76947C7.03969 1.3335 8.15979 1.3335 10.4 1.3335H18.6667L28 10.6668V24.2668C28 26.507 28 27.6271 27.564 28.4828C27.1805 29.2354 26.5686 29.8474 25.816 30.2309C24.9603 30.6668 23.8402 30.6668 21.6 30.6668H10.4C8.15979 30.6668 7.03969 30.6668 6.18404 30.2309C5.43139 29.8474 4.81947 29.2354 4.43597 28.4828C4 27.6271 4 26.507 4 24.2668V7.73349Z" fill="#EC5B27"/>
+</g>
+<g opacity="0.96">
+<path d="M10.2704 24.0002V18.3042H8.87042V20.4962H7.38242V18.3042H5.98242V24.0002H7.38242V21.7442H8.87042V24.0002H10.2704Z" fill="white"/>
+<path d="M15.2839 19.5522V18.3042H11.0839V19.5522H12.4839V24.0002H13.8839V19.5522H15.2839Z" fill="white"/>
+<path d="M21.4116 24.0002V18.3042H20.0356L18.7556 20.8162L17.4756 18.3042H16.0996V24.0002H17.4996V21.2722L18.3076 22.6802H19.2036L20.0116 21.2722V24.0002H21.4116Z" fill="white"/>
+<path d="M26.3525 24.0002V22.7522H23.9605V18.3042H22.5605V24.0002H26.3525Z" fill="white"/>
+</g>
+<path opacity="0.5" d="M18.6665 1.3335L27.9998 10.6668H21.3332C19.8604 10.6668 18.6665 9.47292 18.6665 8.00016V1.3335Z" fill="white"/>
+<defs>
+<filter id="filter0_d_3055_14424" x="2" y="0.333496" width="28" height="33.3335" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dy="1"/>
+<feGaussianBlur stdDeviation="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3055_14424"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3055_14424" result="shape"/>
+</filter>
+</defs>
+</svg>

File diff suppressed because it is too large
+ 23 - 0
web/app/components/base/icons/assets/public/files/json.svg


+ 22 - 0
web/app/components/base/icons/assets/public/files/pdf.svg

@@ -0,0 +1,22 @@
+<svg width="32" height="34" viewBox="0 0 32 34" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g filter="url(#filter0_d_3055_14420)">
+<path d="M4 7.73349C4 5.49329 4 4.37318 4.43597 3.51753C4.81947 2.76489 5.43139 2.15296 6.18404 1.76947C7.03969 1.3335 8.15979 1.3335 10.4 1.3335H18.6667L28 10.6668V24.2668C28 26.507 28 27.6271 27.564 28.4828C27.1805 29.2354 26.5686 29.8474 25.816 30.2309C24.9603 30.6668 23.8402 30.6668 21.6 30.6668H10.4C8.15979 30.6668 7.03969 30.6668 6.18404 30.2309C5.43139 29.8474 4.81947 29.2354 4.43597 28.4828C4 27.6271 4 26.507 4 24.2668V7.73349Z" fill="#DD3633"/>
+</g>
+<g opacity="0.96">
+<path d="M13.2801 20.1362C13.2801 19.2002 12.6001 18.3042 11.3361 18.3042H9.08008V24.0002H10.4801V21.9682H11.3361C12.6001 21.9682 13.2801 21.0722 13.2801 20.1362ZM11.8801 20.1362C11.8801 20.4322 11.6561 20.7122 11.2721 20.7122H10.4801V19.5602H11.2721C11.6561 19.5602 11.8801 19.8402 11.8801 20.1362Z" fill="white"/>
+<path d="M18.3357 21.1522C18.3357 20.2562 18.4077 19.5282 17.7437 18.8642C17.3517 18.4722 16.7997 18.3042 16.2077 18.3042H14.0957V24.0002H16.2077C16.7997 24.0002 17.3517 23.8322 17.7437 23.4402C18.4077 22.7762 18.3357 22.0482 18.3357 21.1522ZM16.9357 21.1522C16.9357 22.1202 16.8957 22.2722 16.7837 22.4322C16.6557 22.6242 16.4637 22.7522 16.1117 22.7522H15.4957V19.5522H16.1117C16.4637 19.5522 16.6557 19.6802 16.7837 19.8722C16.8957 20.0322 16.9357 20.1922 16.9357 21.1522Z" fill="white"/>
+<path d="M23.1786 19.5522V18.3042H19.3066V24.0002H20.7066V21.8002H22.8186V20.5522H20.7066V19.5522H23.1786Z" fill="white"/>
+</g>
+<path opacity="0.5" d="M18.6665 1.3335L27.9998 10.6668H21.3332C19.8604 10.6668 18.6665 9.47292 18.6665 8.00016V1.3335Z" fill="white"/>
+<defs>
+<filter id="filter0_d_3055_14420" x="2" y="0.333496" width="28" height="33.3335" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dy="1"/>
+<feGaussianBlur stdDeviation="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_3055_14420"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_3055_14420" result="shape"/>
+</filter>
+</defs>
+</svg>

File diff suppressed because it is too large
+ 23 - 0
web/app/components/base/icons/assets/public/files/txt.svg


File diff suppressed because it is too large
+ 23 - 0
web/app/components/base/icons/assets/public/files/unknow.svg


+ 18 - 0
web/app/components/base/icons/assets/public/files/xlsx.svg

@@ -0,0 +1,18 @@
+<svg width="24" height="26" viewBox="0 0 24 26" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g filter="url(#filter0_d_5938_927)">
+<path d="M3 5.8C3 4.11984 3 3.27976 3.32698 2.63803C3.6146 2.07354 4.07354 1.6146 4.63803 1.32698C5.27976 1 6.11984 1 7.8 1H14L21 8V18.2C21 19.8802 21 20.7202 20.673 21.362C20.3854 21.9265 19.9265 22.3854 19.362 22.673C18.7202 23 17.8802 23 16.2 23H7.8C6.11984 23 5.27976 23 4.63803 22.673C4.07354 22.3854 3.6146 21.9265 3.32698 21.362C3 20.7202 3 19.8802 3 18.2V5.8Z" fill="#169951"/>
+</g>
+<path opacity="0.5" d="M14 1L21 8H16C14.8954 8 14 7.10457 14 6V1Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M17 12C17.5523 12 18 12.4477 18 13V18C18 18.5523 17.5523 19 17 19H7C6.44772 19 6 18.5523 6 18V13C6 12.4477 6.44772 12 7 12H17ZM11.5 13H7L7 15H11.5V13ZM12.5 18H17V16H12.5V18ZM11.5 16V18H7L7 16H11.5ZM12.5 15H17V13H12.5V15Z" fill="white" fill-opacity="0.96"/>
+<defs>
+<filter id="filter0_d_5938_927" x="1" y="0" width="22" height="26" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dy="1"/>
+<feGaussianBlur stdDeviation="1"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5938_927"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5938_927" result="shape"/>
+</filter>
+</defs>
+</svg>

File diff suppressed because it is too large
+ 5 - 0
web/app/components/base/icons/assets/vender/line/editor/bezier-curve-03.svg


File diff suppressed because it is too large
+ 5 - 0
web/app/components/base/icons/assets/vender/line/editor/type-square.svg


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

@@ -0,0 +1,10 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Left Icon" clip-path="url(#clip0_10386_42171)">
+<path id="Icon" d="M7.99998 4V2.5L9.49998 1L9.99998 2L11 2.5L9.49998 4H7.99998ZM7.99998 4L5.99999 5.99997M11 6C11 8.76142 8.76142 11 6 11C3.23858 11 1 8.76142 1 6C1 3.23858 3.23858 1 6 1M8.5 6C8.5 7.38071 7.38071 8.5 6 8.5C4.61929 8.5 3.5 7.38071 3.5 6C3.5 4.61929 4.61929 3.5 6 3.5" stroke="#667085" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+<defs>
+<clipPath id="clip0_10386_42171">
+<rect width="12" height="12" fill="white"/>
+</clipPath>
+</defs>
+</svg>

File diff suppressed because it is too large
+ 5 - 0
web/app/components/base/icons/assets/vender/solid/editor/citations.svg


File diff suppressed because it is too large
+ 83 - 0
web/app/components/base/icons/src/public/common/Notion.json


+ 16 - 0
web/app/components/base/icons/src/public/common/Notion.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Notion.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 = 'Notion'
+
+export default Icon

+ 1 - 0
web/app/components/base/icons/src/public/common/index.ts

@@ -1,3 +1,4 @@
 export { default as Dify } from './Dify'
 export { default as Github } from './Github'
 export { default as MessageChatSquare } from './MessageChatSquare'
+export { default as Notion } from './Notion'

+ 178 - 0
web/app/components/base/icons/src/public/files/Html.json

@@ -0,0 +1,178 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "32",
+			"height": "34",
+			"viewBox": "0 0 32 34",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"filter": "url(#filter0_d_3055_14424)"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"d": "M4 7.73349C4 5.49329 4 4.37318 4.43597 3.51753C4.81947 2.76489 5.43139 2.15296 6.18404 1.76947C7.03969 1.3335 8.15979 1.3335 10.4 1.3335H18.6667L28 10.6668V24.2668C28 26.507 28 27.6271 27.564 28.4828C27.1805 29.2354 26.5686 29.8474 25.816 30.2309C24.9603 30.6668 23.8402 30.6668 21.6 30.6668H10.4C8.15979 30.6668 7.03969 30.6668 6.18404 30.2309C5.43139 29.8474 4.81947 29.2354 4.43597 28.4828C4 27.6271 4 26.507 4 24.2668V7.73349Z",
+							"fill": "#EC5B27"
+						},
+						"children": []
+					}
+				]
+			},
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"opacity": "0.96"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"d": "M10.2704 24.0002V18.3042H8.87042V20.4962H7.38242V18.3042H5.98242V24.0002H7.38242V21.7442H8.87042V24.0002H10.2704Z",
+							"fill": "white"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"d": "M15.2839 19.5522V18.3042H11.0839V19.5522H12.4839V24.0002H13.8839V19.5522H15.2839Z",
+							"fill": "white"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"d": "M21.4116 24.0002V18.3042H20.0356L18.7556 20.8162L17.4756 18.3042H16.0996V24.0002H17.4996V21.2722L18.3076 22.6802H19.2036L20.0116 21.2722V24.0002H21.4116Z",
+							"fill": "white"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"d": "M26.3525 24.0002V22.7522H23.9605V18.3042H22.5605V24.0002H26.3525Z",
+							"fill": "white"
+						},
+						"children": []
+					}
+				]
+			},
+			{
+				"type": "element",
+				"name": "path",
+				"attributes": {
+					"opacity": "0.5",
+					"d": "M18.6665 1.3335L27.9998 10.6668H21.3332C19.8604 10.6668 18.6665 9.47292 18.6665 8.00016V1.3335Z",
+					"fill": "white"
+				},
+				"children": []
+			},
+			{
+				"type": "element",
+				"name": "defs",
+				"attributes": {},
+				"children": [
+					{
+						"type": "element",
+						"name": "filter",
+						"attributes": {
+							"id": "filter0_d_3055_14424",
+							"x": "2",
+							"y": "0.333496",
+							"width": "28",
+							"height": "33.3335",
+							"filterUnits": "userSpaceOnUse",
+							"color-interpolation-filters": "sRGB"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "feFlood",
+								"attributes": {
+									"flood-opacity": "0",
+									"result": "BackgroundImageFix"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feColorMatrix",
+								"attributes": {
+									"in": "SourceAlpha",
+									"type": "matrix",
+									"values": "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0",
+									"result": "hardAlpha"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feOffset",
+								"attributes": {
+									"dy": "1"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feGaussianBlur",
+								"attributes": {
+									"stdDeviation": "1"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feColorMatrix",
+								"attributes": {
+									"type": "matrix",
+									"values": "0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feBlend",
+								"attributes": {
+									"mode": "normal",
+									"in2": "BackgroundImageFix",
+									"result": "effect1_dropShadow_3055_14424"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feBlend",
+								"attributes": {
+									"mode": "normal",
+									"in": "SourceGraphic",
+									"in2": "effect1_dropShadow_3055_14424",
+									"result": "shape"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "Html"
+}

+ 16 - 0
web/app/components/base/icons/src/public/files/Html.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Html.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 = 'Html'
+
+export default Icon

File diff suppressed because it is too large
+ 178 - 0
web/app/components/base/icons/src/public/files/Json.json


+ 16 - 0
web/app/components/base/icons/src/public/files/Json.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Json.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 = 'Json'
+
+export default Icon

+ 169 - 0
web/app/components/base/icons/src/public/files/Pdf.json

@@ -0,0 +1,169 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "32",
+			"height": "34",
+			"viewBox": "0 0 32 34",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"filter": "url(#filter0_d_3055_14420)"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"d": "M4 7.73349C4 5.49329 4 4.37318 4.43597 3.51753C4.81947 2.76489 5.43139 2.15296 6.18404 1.76947C7.03969 1.3335 8.15979 1.3335 10.4 1.3335H18.6667L28 10.6668V24.2668C28 26.507 28 27.6271 27.564 28.4828C27.1805 29.2354 26.5686 29.8474 25.816 30.2309C24.9603 30.6668 23.8402 30.6668 21.6 30.6668H10.4C8.15979 30.6668 7.03969 30.6668 6.18404 30.2309C5.43139 29.8474 4.81947 29.2354 4.43597 28.4828C4 27.6271 4 26.507 4 24.2668V7.73349Z",
+							"fill": "#DD3633"
+						},
+						"children": []
+					}
+				]
+			},
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"opacity": "0.96"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"d": "M13.2801 20.1362C13.2801 19.2002 12.6001 18.3042 11.3361 18.3042H9.08008V24.0002H10.4801V21.9682H11.3361C12.6001 21.9682 13.2801 21.0722 13.2801 20.1362ZM11.8801 20.1362C11.8801 20.4322 11.6561 20.7122 11.2721 20.7122H10.4801V19.5602H11.2721C11.6561 19.5602 11.8801 19.8402 11.8801 20.1362Z",
+							"fill": "white"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"d": "M18.3357 21.1522C18.3357 20.2562 18.4077 19.5282 17.7437 18.8642C17.3517 18.4722 16.7997 18.3042 16.2077 18.3042H14.0957V24.0002H16.2077C16.7997 24.0002 17.3517 23.8322 17.7437 23.4402C18.4077 22.7762 18.3357 22.0482 18.3357 21.1522ZM16.9357 21.1522C16.9357 22.1202 16.8957 22.2722 16.7837 22.4322C16.6557 22.6242 16.4637 22.7522 16.1117 22.7522H15.4957V19.5522H16.1117C16.4637 19.5522 16.6557 19.6802 16.7837 19.8722C16.8957 20.0322 16.9357 20.1922 16.9357 21.1522Z",
+							"fill": "white"
+						},
+						"children": []
+					},
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"d": "M23.1786 19.5522V18.3042H19.3066V24.0002H20.7066V21.8002H22.8186V20.5522H20.7066V19.5522H23.1786Z",
+							"fill": "white"
+						},
+						"children": []
+					}
+				]
+			},
+			{
+				"type": "element",
+				"name": "path",
+				"attributes": {
+					"opacity": "0.5",
+					"d": "M18.6665 1.3335L27.9998 10.6668H21.3332C19.8604 10.6668 18.6665 9.47292 18.6665 8.00016V1.3335Z",
+					"fill": "white"
+				},
+				"children": []
+			},
+			{
+				"type": "element",
+				"name": "defs",
+				"attributes": {},
+				"children": [
+					{
+						"type": "element",
+						"name": "filter",
+						"attributes": {
+							"id": "filter0_d_3055_14420",
+							"x": "2",
+							"y": "0.333496",
+							"width": "28",
+							"height": "33.3335",
+							"filterUnits": "userSpaceOnUse",
+							"color-interpolation-filters": "sRGB"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "feFlood",
+								"attributes": {
+									"flood-opacity": "0",
+									"result": "BackgroundImageFix"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feColorMatrix",
+								"attributes": {
+									"in": "SourceAlpha",
+									"type": "matrix",
+									"values": "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0",
+									"result": "hardAlpha"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feOffset",
+								"attributes": {
+									"dy": "1"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feGaussianBlur",
+								"attributes": {
+									"stdDeviation": "1"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feColorMatrix",
+								"attributes": {
+									"type": "matrix",
+									"values": "0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feBlend",
+								"attributes": {
+									"mode": "normal",
+									"in2": "BackgroundImageFix",
+									"result": "effect1_dropShadow_3055_14420"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feBlend",
+								"attributes": {
+									"mode": "normal",
+									"in": "SourceGraphic",
+									"in2": "effect1_dropShadow_3055_14420",
+									"result": "shape"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "Pdf"
+}

+ 16 - 0
web/app/components/base/icons/src/public/files/Pdf.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Pdf.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 = 'Pdf'
+
+export default Icon

File diff suppressed because it is too large
+ 180 - 0
web/app/components/base/icons/src/public/files/Txt.json


+ 16 - 0
web/app/components/base/icons/src/public/files/Txt.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Txt.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 = 'Txt'
+
+export default Icon

File diff suppressed because it is too large
+ 199 - 0
web/app/components/base/icons/src/public/files/Unknow.json


+ 16 - 0
web/app/components/base/icons/src/public/files/Unknow.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Unknow.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 = 'Unknow'
+
+export default Icon

+ 145 - 0
web/app/components/base/icons/src/public/files/Xlsx.json

@@ -0,0 +1,145 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "24",
+			"height": "26",
+			"viewBox": "0 0 24 26",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"filter": "url(#filter0_d_5938_927)"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"d": "M3 5.8C3 4.11984 3 3.27976 3.32698 2.63803C3.6146 2.07354 4.07354 1.6146 4.63803 1.32698C5.27976 1 6.11984 1 7.8 1H14L21 8V18.2C21 19.8802 21 20.7202 20.673 21.362C20.3854 21.9265 19.9265 22.3854 19.362 22.673C18.7202 23 17.8802 23 16.2 23H7.8C6.11984 23 5.27976 23 4.63803 22.673C4.07354 22.3854 3.6146 21.9265 3.32698 21.362C3 20.7202 3 19.8802 3 18.2V5.8Z",
+							"fill": "#169951"
+						},
+						"children": []
+					}
+				]
+			},
+			{
+				"type": "element",
+				"name": "path",
+				"attributes": {
+					"opacity": "0.5",
+					"d": "M14 1L21 8H16C14.8954 8 14 7.10457 14 6V1Z",
+					"fill": "white"
+				},
+				"children": []
+			},
+			{
+				"type": "element",
+				"name": "path",
+				"attributes": {
+					"fill-rule": "evenodd",
+					"clip-rule": "evenodd",
+					"d": "M17 12C17.5523 12 18 12.4477 18 13V18C18 18.5523 17.5523 19 17 19H7C6.44772 19 6 18.5523 6 18V13C6 12.4477 6.44772 12 7 12H17ZM11.5 13H7L7 15H11.5V13ZM12.5 18H17V16H12.5V18ZM11.5 16V18H7L7 16H11.5ZM12.5 15H17V13H12.5V15Z",
+					"fill": "white",
+					"fill-opacity": "0.96"
+				},
+				"children": []
+			},
+			{
+				"type": "element",
+				"name": "defs",
+				"attributes": {},
+				"children": [
+					{
+						"type": "element",
+						"name": "filter",
+						"attributes": {
+							"id": "filter0_d_5938_927",
+							"x": "1",
+							"y": "0",
+							"width": "22",
+							"height": "26",
+							"filterUnits": "userSpaceOnUse",
+							"color-interpolation-filters": "sRGB"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "feFlood",
+								"attributes": {
+									"flood-opacity": "0",
+									"result": "BackgroundImageFix"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feColorMatrix",
+								"attributes": {
+									"in": "SourceAlpha",
+									"type": "matrix",
+									"values": "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0",
+									"result": "hardAlpha"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feOffset",
+								"attributes": {
+									"dy": "1"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feGaussianBlur",
+								"attributes": {
+									"stdDeviation": "1"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feColorMatrix",
+								"attributes": {
+									"type": "matrix",
+									"values": "0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feBlend",
+								"attributes": {
+									"mode": "normal",
+									"in2": "BackgroundImageFix",
+									"result": "effect1_dropShadow_5938_927"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feBlend",
+								"attributes": {
+									"mode": "normal",
+									"in": "SourceGraphic",
+									"in2": "effect1_dropShadow_5938_927",
+									"result": "shape"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "Xlsx"
+}

+ 16 - 0
web/app/components/base/icons/src/public/files/Xlsx.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Xlsx.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 = 'Xlsx'
+
+export default Icon

+ 6 - 0
web/app/components/base/icons/src/public/files/index.ts

@@ -1,2 +1,8 @@
 export { default as Csv } from './Csv'
+export { default as Html } from './Html'
+export { default as Json } from './Json'
 export { default as Md } from './Md'
+export { default as Pdf } from './Pdf'
+export { default as Txt } from './Txt'
+export { default as Unknow } from './Unknow'
+export { default as Xlsx } from './Xlsx'

+ 2 - 0
web/app/components/base/icons/src/public/llm/Localai.tsx

@@ -11,4 +11,6 @@ const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseP
   ref,
 ) => <IconBase {...props} ref={ref} data={data as IconData} />)
 
+Icon.displayName = 'Localai'
+
 export default Icon

+ 2 - 0
web/app/components/base/icons/src/public/llm/LocalaiText.tsx

@@ -11,4 +11,6 @@ const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseP
   ref,
 ) => <IconBase {...props} ref={ref} data={data as IconData} />)
 
+Icon.displayName = 'LocalaiText'
+
 export default Icon

File diff suppressed because it is too large
+ 38 - 0
web/app/components/base/icons/src/vender/line/editor/BezierCurve03.json


+ 16 - 0
web/app/components/base/icons/src/vender/line/editor/BezierCurve03.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './BezierCurve03.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 = 'BezierCurve03'
+
+export default Icon

File diff suppressed because it is too large
+ 38 - 0
web/app/components/base/icons/src/vender/line/editor/TypeSquare.json


+ 16 - 0
web/app/components/base/icons/src/vender/line/editor/TypeSquare.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './TypeSquare.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 = 'TypeSquare'
+
+export default Icon

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

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

+ 65 - 0
web/app/components/base/icons/src/vender/line/general/Target04.json

@@ -0,0 +1,65 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "12",
+			"height": "12",
+			"viewBox": "0 0 12 12",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "Left Icon",
+					"clip-path": "url(#clip0_10386_42171)"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon",
+							"d": "M7.99998 4V2.5L9.49998 1L9.99998 2L11 2.5L9.49998 4H7.99998ZM7.99998 4L5.99999 5.99997M11 6C11 8.76142 8.76142 11 6 11C3.23858 11 1 8.76142 1 6C1 3.23858 3.23858 1 6 1M8.5 6C8.5 7.38071 7.38071 8.5 6 8.5C4.61929 8.5 3.5 7.38071 3.5 6C3.5 4.61929 4.61929 3.5 6 3.5",
+							"stroke": "currentColor",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			},
+			{
+				"type": "element",
+				"name": "defs",
+				"attributes": {},
+				"children": [
+					{
+						"type": "element",
+						"name": "clipPath",
+						"attributes": {
+							"id": "clip0_10386_42171"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "rect",
+								"attributes": {
+									"width": "12",
+									"height": "12",
+									"fill": "white"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "Target04"
+}

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

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Target04.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 = 'Target04'
+
+export default Icon

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

@@ -12,6 +12,7 @@ export { default as LogOut01 } from './LogOut01'
 export { default as Pin02 } from './Pin02'
 export { default as Plus } from './Plus'
 export { default as SearchLg } from './SearchLg'
+export { default as Target04 } from './Target04'
 export { default as Trash03 } from './Trash03'
 export { default as XClose } from './XClose'
 export { default as X } from './X'

File diff suppressed because it is too large
+ 36 - 0
web/app/components/base/icons/src/vender/solid/editor/Citations.json


+ 16 - 0
web/app/components/base/icons/src/vender/solid/editor/Citations.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Citations.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 = 'Citations'
+
+export default Icon

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

@@ -1 +1,2 @@
 export { default as Brush01 } from './Brush01'
+export { default as Citations } from './Citations'

+ 2 - 2
web/app/components/base/portal-to-follow-elem/index.tsx

@@ -15,7 +15,7 @@ import {
   useRole,
 } from '@floating-ui/react'
 
-import type { Placement } from '@floating-ui/react'
+import type { OffsetOptions, Placement } from '@floating-ui/react'
 
 type PortalToFollowElemOptions = {
   /*
@@ -25,7 +25,7 @@ type PortalToFollowElemOptions = {
   */
   placement?: Placement
   open?: boolean
-  offset?: number
+  offset?: number | OffsetOptions
   onOpenChange?: (open: boolean) => void
 }
 

+ 1 - 1
web/app/components/explore/item-operation/index.tsx

@@ -11,7 +11,7 @@ import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigge
 
 export type IItemOperationProps = {
   className?: string
-  isItemHovering: boolean
+  isItemHovering?: boolean
   isPinned: boolean
   isShowRenameConversation?: boolean
   onRenameConversation?: () => void

+ 18 - 1
web/app/components/explore/universal-chat/index.tsx

@@ -199,6 +199,7 @@ const Main: FC<IMainProps> = () => {
 
   const [suggestedQuestionsAfterAnswerConfig, setSuggestedQuestionsAfterAnswerConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null)
   const [speechToTextConfig, setSpeechToTextConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null)
+  const [citationConfig, setCitationConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null)
 
   const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false)
 
@@ -271,6 +272,7 @@ const Main: FC<IMainProps> = () => {
             content: item.answer,
             feedback: item.feedback,
             isAnswer: true,
+            citation: item.retriever_resources,
           })
         })
         setChatList(newChatList)
@@ -374,7 +376,7 @@ const Main: FC<IMainProps> = () => {
         const isNotNewConversation = allConversations.some(item => item.id === _conversationId)
         setAllConversationList(allConversations)
         // fetch new conversation info
-        const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text }: any = appParams
+        const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, retriever_resource }: any = appParams
         const prompt_variables = userInputsFormToPromptVariables(user_input_form)
 
         setNewConversationInfo({
@@ -387,6 +389,7 @@ const Main: FC<IMainProps> = () => {
         } as PromptConfig)
         setSuggestedQuestionsAfterAnswerConfig(suggested_questions_after_answer)
         setSpeechToTextConfig(speech_to_text)
+        setCitationConfig(retriever_resource)
 
         if (isNotNewConversation)
           setCurrConversationId(_conversationId, APP_ID, false)
@@ -593,6 +596,19 @@ const Main: FC<IMainProps> = () => {
           })
         setChatList(newListWithAnswer)
       },
+      onMessageEnd: (messageEnd) => {
+        responseItem.citation = messageEnd.retriever_resources
+
+        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 })
+          })
+        setChatList(newListWithAnswer)
+      },
       onError() {
         setErrorHappened(true)
         // role back placeholder answer
@@ -783,6 +799,7 @@ const Main: FC<IMainProps> = () => {
                 isShowSuggestion={doShowSuggestion}
                 suggestionList={suggestQuestions}
                 isShowSpeechToText={speechToTextConfig?.enabled}
+                isShowCitation={citationConfig?.enabled}
                 dataSets={dataSets}
               />
             </div>

+ 23 - 2
web/app/components/share/chat/index.tsx

@@ -167,6 +167,7 @@ const Main: FC<IMainProps> = ({
 
   const [suggestedQuestionsAfterAnswerConfig, setSuggestedQuestionsAfterAnswerConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null)
   const [speechToTextConfig, setSpeechToTextConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null)
+  const [citationConfig, setCitationConfig] = useState<SuggestedQuestionsAfterAnswerConfig | null>(null)
 
   const [conversationIdChangeBecauseOfNew, setConversationIdChangeBecauseOfNew, getConversationIdChangeBecauseOfNew] = useGetState(false)
   const [isChatStarted, { setTrue: setChatStarted, setFalse: setChatNotStarted }] = useBoolean(false)
@@ -249,6 +250,7 @@ const Main: FC<IMainProps> = ({
             content: item.answer,
             feedback: item.feedback,
             isAnswer: true,
+            citation: item.retriever_resources,
           })
         })
         setChatList(newChatList)
@@ -364,7 +366,7 @@ const Main: FC<IMainProps> = ({
         const isNotNewConversation = allConversations.some(item => item.id === _conversationId)
         setAllConversationList(allConversations)
         // fetch new conversation info
-        const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text }: any = appParams
+        const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, retriever_resource }: any = appParams
         const prompt_variables = userInputsFormToPromptVariables(user_input_form)
         if (siteInfo.default_language)
           changeLanguage(siteInfo.default_language)
@@ -380,6 +382,7 @@ const Main: FC<IMainProps> = ({
         } as PromptConfig)
         setSuggestedQuestionsAfterAnswerConfig(suggested_questions_after_answer)
         setSpeechToTextConfig(speech_to_text)
+        setCitationConfig(retriever_resource)
 
         // setConversationList(conversations as ConversationItem[])
 
@@ -474,7 +477,7 @@ const Main: FC<IMainProps> = ({
     setChatList(newList)
 
     // answer
-    const responseItem = {
+    const responseItem: IChatItem = {
       id: `${Date.now()}`,
       content: '',
       isAnswer: true,
@@ -533,6 +536,23 @@ const Main: FC<IMainProps> = ({
           setIsShowSuggestion(true)
         }
       },
+      onMessageEnd: isInstalledApp
+        ? (messageEnd) => {
+          if (!isInstalledApp)
+            return
+          responseItem.citation = messageEnd.retriever_resources
+
+          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 })
+            })
+          setChatList(newListWithAnswer)
+        }
+        : undefined,
       onError() {
         setResponsingFalse()
         // role back placeholder answer
@@ -678,6 +698,7 @@ const Main: FC<IMainProps> = ({
                     isShowSuggestion={doShowSuggestion}
                     suggestionList={suggestQuestions}
                     isShowSpeechToText={speechToTextConfig?.enabled}
+                    isShowCitation={citationConfig?.enabled && isInstalledApp}
                   />
                 </div>
               </div>)

+ 3 - 2
web/app/components/share/chatbot/index.tsx

@@ -188,6 +188,7 @@ const Main: FC<IMainProps> = ({
             content: item.answer,
             feedback: item.feedback,
             isAnswer: true,
+            citation: item.retriever_resources,
           })
         })
         setChatList(newChatList)
@@ -289,7 +290,7 @@ const Main: FC<IMainProps> = ({
         const isNotNewConversation = allConversations.some(item => item.id === _conversationId)
         setAllConversationList(allConversations)
         // fetch new conversation info
-        const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text }: any = appParams
+        const { user_input_form, opening_statement: introduction, suggested_questions_after_answer, speech_to_text, retriever_resource }: any = appParams
         const prompt_variables = userInputsFormToPromptVariables(user_input_form)
         if (siteInfo.default_language)
           changeLanguage(siteInfo.default_language)
@@ -399,7 +400,7 @@ const Main: FC<IMainProps> = ({
     setChatList(newList)
 
     // answer
-    const responseItem = {
+    const responseItem: IChatItem = {
       id: `${Date.now()}`,
       content: '',
       isAnswer: true,

+ 7 - 1
web/context/debug-configuration.ts

@@ -1,5 +1,5 @@
 import { createContext } from 'use-context-selector'
-import type { CompletionParams, Inputs, ModelConfig, MoreLikeThisConfig, PromptConfig, SpeechToTextConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug'
+import type { CitationConfig, CompletionParams, Inputs, ModelConfig, MoreLikeThisConfig, PromptConfig, SpeechToTextConfig, SuggestedQuestionsAfterAnswerConfig } from '@/models/debug'
 import type { DataSet } from '@/models/datasets'
 
 type IDebugConfiguration = {
@@ -21,6 +21,8 @@ type IDebugConfiguration = {
   setSuggestedQuestionsAfterAnswerConfig: (suggestedQuestionsAfterAnswerConfig: SuggestedQuestionsAfterAnswerConfig) => void
   speechToTextConfig: SpeechToTextConfig
   setSpeechToTextConfig: (speechToTextConfig: SpeechToTextConfig) => void
+  citationConfig: CitationConfig
+  setCitationConfig: (citationConfig: CitationConfig) => void
   formattingChanged: boolean
   setFormattingChanged: (formattingChanged: boolean) => void
   inputs: Inputs
@@ -65,6 +67,10 @@ const DebugConfigurationContext = createContext<IDebugConfiguration>({
     enabled: false,
   },
   setSpeechToTextConfig: () => { },
+  citationConfig: {
+    enabled: false,
+  },
+  setCitationConfig: () => {},
   formattingChanged: false,
   setFormattingChanged: () => { },
   inputs: {},

+ 5 - 0
web/i18n/lang/app-debug.en.ts

@@ -51,6 +51,11 @@ const translation = {
       description: 'Once enabled, you can use voice input.',
       resDes: 'Voice input is enabled',
     },
+    citation: {
+      title: 'Citations and Attributions',
+      description: 'Once enabled, show source document and attributed section of the generated content.',
+      resDes: 'Citations and Attributions is enabled',
+    },
     dataSet: {
       title: 'Context',
       noData: 'You can import datasets as context',

+ 5 - 0
web/i18n/lang/app-debug.zh.ts

@@ -51,6 +51,11 @@ const translation = {
       description: '启用后,您可以使用语音输入。',
       resDes: '语音输入已启用',
     },
+    citation: {
+      title: '引用和归属',
+      description: '启用后,显示源文档和生成内容的归属部分。',
+      resDes: '引用和归属已启用',
+    },
     dataSet: {
       title: '上下文',
       noData: '您可以导入数据集作为上下文',

+ 8 - 0
web/i18n/lang/common.en.ts

@@ -343,6 +343,14 @@ const translation = {
     conversationName: 'Conversation name',
     conversationNamePlaceholder: 'Please input conversation name',
     conversationNameCanNotEmpty: 'Conversation name required',
+    citation: {
+      title: 'CITATIONS',
+      linkToDataset: 'Link to dataset',
+      characters: 'Characters:',
+      hitCount: 'Hit count:',
+      vectorHash: 'Vector hash:',
+      hitScore: 'Hit Score:',
+    },
   },
 }
 

+ 8 - 0
web/i18n/lang/common.zh.ts

@@ -343,6 +343,14 @@ const translation = {
     conversationName: '会话名称',
     conversationNamePlaceholder: '请输入会话名称',
     conversationNameCanNotEmpty: '会话名称必填',
+    citation: {
+      title: '引用',
+      linkToDataset: '去往数据集',
+      characters: '字符:',
+      hitCount: '命中次数:',
+      vectorHash: '向量哈希:',
+      hitScore: '命中得分:',
+    },
   },
 }
 

+ 5 - 0
web/models/debug.ts

@@ -33,6 +33,8 @@ export type SuggestedQuestionsAfterAnswerConfig = MoreLikeThisConfig
 
 export type SpeechToTextConfig = MoreLikeThisConfig
 
+export type CitationConfig = MoreLikeThisConfig
+
 // frontend use. Not the same as backend
 export type ModelConfig = {
   provider: string // LLM Provider: for example "OPENAI"
@@ -48,6 +50,9 @@ export type ModelConfig = {
   speech_to_text: {
     enabled: boolean
   } | null
+  retriever_resource: {
+    enabled: boolean
+  } | null
   dataSets: any[]
 }
 

+ 9 - 5
web/service/base.ts

@@ -1,7 +1,7 @@
 /* eslint-disable no-new, prefer-promise-reject-errors */
 import { API_PREFIX, IS_CE_EDITION, PUBLIC_API_PREFIX } from '@/config'
 import Toast from '@/app/components/base/toast'
-import type { ThoughtItem } from '@/app/components/app/chat/type'
+import type { MessageEnd, ThoughtItem } from '@/app/components/app/chat/type'
 
 const TIME_OUT = 100000
 
@@ -33,6 +33,7 @@ export type IOnDataMoreInfo = {
 
 export type IOnData = (message: string, isFirstMessage: boolean, moreInfo: IOnDataMoreInfo) => void
 export type IOnThought = (though: ThoughtItem) => void
+export type IOnMessageEnd = (messageEnd: MessageEnd) => void
 export type IOnCompleted = (hasError?: boolean) => void
 export type IOnError = (msg: string, code?: string) => void
 
@@ -43,6 +44,7 @@ type IOtherOptions = {
   deleteContentType?: boolean
   onData?: IOnData // for stream
   onThought?: IOnThought
+  onMessageEnd?: IOnMessageEnd
   onError?: IOnError
   onCompleted?: IOnCompleted // for stream
   getAbortController?: (abortController: AbortController) => void
@@ -65,7 +67,7 @@ export function format(text: string) {
   return res.replaceAll('\n', '<br/>').replaceAll('```', '')
 }
 
-const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted, onThought?: IOnThought) => {
+const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted, onThought?: IOnThought, onMessageEnd?: IOnMessageEnd) => {
   if (!response.ok)
     throw new Error('Network response was not ok')
 
@@ -86,7 +88,6 @@ const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted
       try {
         lines.forEach((message) => {
           if (message.startsWith('data: ')) { // check if it starts with data:
-            // console.log(message);
             try {
               bufferObj = JSON.parse(message.substring(6)) // remove data: and parse as json
             }
@@ -121,6 +122,9 @@ const handleStream = (response: any, onData: IOnData, onCompleted?: IOnCompleted
             else if (bufferObj.event === 'agent_thought') {
               onThought?.(bufferObj as any)
             }
+            else if (bufferObj.event === 'message_end') {
+              onMessageEnd?.(bufferObj as any)
+            }
           }
         })
         buffer = lines[lines.length - 1]
@@ -311,7 +315,7 @@ export const upload = (options: any): Promise<any> => {
   })
 }
 
-export const ssePost = (url: string, fetchOptions: any, { isPublicAPI = false, onData, onCompleted, onThought, onError, getAbortController }: IOtherOptions) => {
+export const ssePost = (url: string, fetchOptions: any, { isPublicAPI = false, onData, onCompleted, onThought, onMessageEnd, onError, getAbortController }: IOtherOptions) => {
   const abortController = new AbortController()
 
   const options = Object.assign({}, baseOptions, {
@@ -353,7 +357,7 @@ export const ssePost = (url: string, fetchOptions: any, { isPublicAPI = false, o
           return
         }
         onData?.(str, isFirstMessage, moreInfo)
-      }, onCompleted, onThought)
+      }, onCompleted, onThought, onMessageEnd)
     }).catch((e) => {
       if (e.toString() !== 'AbortError: The user aborted a request.')
         Toast.notify({ type: 'error', message: e })

+ 4 - 3
web/service/debug.ts

@@ -1,9 +1,10 @@
-import type { IOnCompleted, IOnData, IOnError } from './base'
+import type { IOnCompleted, IOnData, IOnError, IOnMessageEnd } from './base'
 import { get, post, ssePost } from './base'
 
-export const sendChatMessage = async (appId: string, body: Record<string, any>, { onData, onCompleted, onError, getAbortController }: {
+export const sendChatMessage = async (appId: string, body: Record<string, any>, { onData, onCompleted, onError, getAbortController, onMessageEnd }: {
   onData: IOnData
   onCompleted: IOnCompleted
+  onMessageEnd: IOnMessageEnd
   onError: IOnError
   getAbortController?: (abortController: AbortController) => void
 }) => {
@@ -12,7 +13,7 @@ export const sendChatMessage = async (appId: string, body: Record<string, any>,
       ...body,
       response_mode: 'streaming',
     },
-  }, { onData, onCompleted, onError, getAbortController })
+  }, { onData, onCompleted, onError, getAbortController, onMessageEnd })
 }
 
 export const stopChatMessageResponding = async (appId: string, taskId: string) => {

+ 4 - 3
web/service/share.ts

@@ -1,4 +1,4 @@
-import type { IOnCompleted, IOnData, IOnError } from './base'
+import type { IOnCompleted, IOnData, IOnError, IOnMessageEnd } from './base'
 import {
   del as consoleDel, get as consoleGet, patch as consolePatch, post as consolePost,
   delPublic as del, getPublic as get, patchPublic as patch, postPublic as post, ssePost,
@@ -22,10 +22,11 @@ function getUrl(url: string, isInstalledApp: boolean, installedAppId: string) {
   return isInstalledApp ? `installed-apps/${installedAppId}/${url.startsWith('/') ? url.slice(1) : url}` : url
 }
 
-export const sendChatMessage = async (body: Record<string, any>, { onData, onCompleted, onError, getAbortController }: {
+export const sendChatMessage = async (body: Record<string, any>, { onData, onCompleted, onError, getAbortController, onMessageEnd }: {
   onData: IOnData
   onCompleted: IOnCompleted
   onError: IOnError
+  onMessageEnd?: IOnMessageEnd
   getAbortController?: (abortController: AbortController) => void
 }, isInstalledApp: boolean, installedAppId = '') => {
   return ssePost(getUrl('chat-messages', isInstalledApp, installedAppId), {
@@ -33,7 +34,7 @@ export const sendChatMessage = async (body: Record<string, any>, { onData, onCom
       ...body,
       response_mode: 'streaming',
     },
-  }, { onData, onCompleted, isPublicAPI: !isInstalledApp, onError, getAbortController })
+  }, { onData, onCompleted, isPublicAPI: !isInstalledApp, onError, getAbortController, onMessageEnd })
 }
 
 export const stopChatMessageResponding = async (appId: string, taskId: string, isInstalledApp: boolean, installedAppId = '') => {

+ 4 - 3
web/service/universal-chat.ts

@@ -1,4 +1,4 @@
-import type { IOnCompleted, IOnData, IOnError, IOnThought } from './base'
+import type { IOnCompleted, IOnData, IOnError, IOnMessageEnd, IOnThought } from './base'
 import {
   del, get, patch, post, ssePost,
 } from './base'
@@ -10,11 +10,12 @@ function getUrl(url: string) {
   return `${baseUrl}/${url.startsWith('/') ? url.slice(1) : url}`
 }
 
-export const sendChatMessage = async (body: Record<string, any>, { onData, onCompleted, onError, onThought, getAbortController }: {
+export const sendChatMessage = async (body: Record<string, any>, { onData, onCompleted, onError, onThought, onMessageEnd, getAbortController }: {
   onData: IOnData
   onCompleted: IOnCompleted
   onError: IOnError
   onThought: IOnThought
+  onMessageEnd: IOnMessageEnd
   getAbortController?: (abortController: AbortController) => void
 }) => {
   return ssePost(getUrl('messages'), {
@@ -22,7 +23,7 @@ export const sendChatMessage = async (body: Record<string, any>, { onData, onCom
       ...body,
       response_mode: 'streaming',
     },
-  }, { onData, onCompleted, onThought, onError, getAbortController })
+  }, { onData, onCompleted, onThought, onError, getAbortController, onMessageEnd })
 }
 
 export const stopChatMessageResponding = async (taskId: string) => {

+ 3 - 0
web/types/app.ts

@@ -99,6 +99,9 @@ export type ModelConfig = {
   speech_to_text: {
     enabled: boolean
   }
+  retriever_resource: {
+    enabled: boolean
+  }
   agent_mode: {
     enabled: boolean
     tools: ToolItem[]