Chore: chat log refactor (#5523)
|
|
@ -1,428 +0,0 @@
|
|||
'use client'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { UserCircleIcon } from '@heroicons/react/24/solid'
|
||||
import cn from 'classnames'
|
||||
import type { CitationItem, DisplayScene, FeedbackFunc, Feedbacktype, IChatItem } from '../type'
|
||||
import OperationBtn from '../operation'
|
||||
import LoadingAnim from '../loading-anim'
|
||||
import { 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 AudioBtn from '@/app/components/base/audio-btn'
|
||||
import { randomString } from '@/utils'
|
||||
import type { MessageRating } from '@/models/log'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
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'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import type { VisionFile } from '@/types/app'
|
||||
import ImageGallery from '@/app/components/base/image-gallery'
|
||||
import Log from '@/app/components/app/chat/log'
|
||||
|
||||
const IconWrapper: FC<{ children: React.ReactNode | string }> = ({ children }) => {
|
||||
return <div className={'rounded-lg h-6 w-6 flex items-center justify-center hover:bg-gray-100'}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
export type IAnswerProps = {
|
||||
item: IChatItem
|
||||
index: number
|
||||
feedbackDisabled: boolean
|
||||
isHideFeedbackEdit: boolean
|
||||
onQueryChange: (query: string) => void
|
||||
onFeedback?: FeedbackFunc
|
||||
displayScene: DisplayScene
|
||||
isResponding?: boolean
|
||||
answerIcon?: ReactNode
|
||||
citation?: CitationItem[]
|
||||
dataSets?: DataSet[]
|
||||
isShowCitation?: boolean
|
||||
isShowCitationHitInfo?: boolean
|
||||
isShowTextToSpeech?: boolean
|
||||
// Annotation props
|
||||
supportAnnotation?: boolean
|
||||
appId?: string
|
||||
question: string
|
||||
onAnnotationEdited?: (question: string, answer: string, index: number) => void
|
||||
onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void
|
||||
onAnnotationRemoved?: (index: number) => void
|
||||
allToolIcons?: Record<string, string | Emoji>
|
||||
isShowPromptLog?: boolean
|
||||
}
|
||||
// The component needs to maintain its own state to control whether to display input component
|
||||
const Answer: FC<IAnswerProps> = ({
|
||||
item,
|
||||
index,
|
||||
onQueryChange,
|
||||
feedbackDisabled = false,
|
||||
isHideFeedbackEdit = false,
|
||||
onFeedback,
|
||||
displayScene = 'web',
|
||||
isResponding,
|
||||
answerIcon,
|
||||
citation,
|
||||
isShowCitation,
|
||||
isShowCitationHitInfo = false,
|
||||
isShowTextToSpeech,
|
||||
supportAnnotation,
|
||||
appId,
|
||||
question,
|
||||
onAnnotationEdited,
|
||||
onAnnotationAdded,
|
||||
onAnnotationRemoved,
|
||||
allToolIcons,
|
||||
isShowPromptLog,
|
||||
}) => {
|
||||
const { id, content, more, feedback, adminFeedback, annotation, agent_thoughts } = item
|
||||
const isAgentMode = !!agent_thoughts && agent_thoughts.length > 0
|
||||
const hasAnnotation = useMemo(() => !!annotation, [annotation])
|
||||
// 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 { t } = useTranslation()
|
||||
|
||||
const [isShowReplyModal, setIsShowReplyModal] = useState(false)
|
||||
|
||||
/**
|
||||
* Render feedback results (distinguish between users and administrators)
|
||||
* User reviews cannot be cancelled in Console
|
||||
* @param rating feedback result
|
||||
* @param isUserFeedback Whether it is user's feedback
|
||||
* @param isWebScene Whether it is web scene
|
||||
* @returns comp
|
||||
*/
|
||||
const renderFeedbackRating = (rating: MessageRating | undefined, isUserFeedback = true, isWebScene = true) => {
|
||||
if (!rating)
|
||||
return null
|
||||
|
||||
const isLike = rating === 'like'
|
||||
const ratingIconClassname = isLike ? 'text-primary-600 bg-primary-100 hover:bg-primary-200' : 'text-red-600 bg-red-100 hover:bg-red-200'
|
||||
const UserSymbol = <UserCircleIcon className='absolute top-[-2px] left-[18px] w-3 h-3 rounded-lg text-gray-400 bg-white' />
|
||||
// The tooltip is always displayed, but the content is different for different scenarios.
|
||||
return (
|
||||
<Tooltip
|
||||
selector={`user-feedback-${randomString(16)}`}
|
||||
content={((isWebScene || (!isUserFeedback && !isWebScene)) ? isLike ? t('appDebug.operation.cancelAgree') : t('appDebug.operation.cancelDisagree') : (!isWebScene && isUserFeedback) ? `${t('appDebug.operation.userAction')}${isLike ? t('appDebug.operation.agree') : t('appDebug.operation.disagree')}` : '') as string}
|
||||
>
|
||||
<div
|
||||
className={`relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-gray-500 hover:text-gray-800 ${(!isWebScene && isUserFeedback) ? '!cursor-default' : ''}`}
|
||||
style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }}
|
||||
{...((isWebScene || (!isUserFeedback && !isWebScene))
|
||||
? {
|
||||
onClick: async () => {
|
||||
const res = await onFeedback?.(id, { rating: null })
|
||||
if (res && !isWebScene)
|
||||
setLocalAdminFeedback({ rating: null })
|
||||
},
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<div className={`${ratingIconClassname} rounded-lg h-6 w-6 flex items-center justify-center`}>
|
||||
<RatingIcon isLike={isLike} />
|
||||
</div>
|
||||
{!isWebScene && isUserFeedback && UserSymbol}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Different scenarios have different operation items.
|
||||
* @param isWebScene Whether it is web scene
|
||||
* @returns comp
|
||||
*/
|
||||
const renderItemOperation = (isWebScene = true) => {
|
||||
const userOperation = () => {
|
||||
return feedback?.rating
|
||||
? null
|
||||
: <div className='flex gap-1'>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.like') as string}>
|
||||
{OperationBtn({ innerContent: <IconWrapper><RatingIcon isLike={true} /></IconWrapper>, onClick: () => onFeedback?.(id, { rating: 'like' }) })}
|
||||
</Tooltip>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.dislike') as string}>
|
||||
{OperationBtn({ innerContent: <IconWrapper><RatingIcon isLike={false} /></IconWrapper>, onClick: () => onFeedback?.(id, { rating: 'dislike' }) })}
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
|
||||
const adminOperation = () => {
|
||||
return <div className='flex gap-1'>
|
||||
{!localAdminFeedback?.rating && <>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.like') as string}>
|
||||
{OperationBtn({
|
||||
innerContent: <IconWrapper><RatingIcon isLike={true} /></IconWrapper>,
|
||||
onClick: async () => {
|
||||
const res = await onFeedback?.(id, { rating: 'like' })
|
||||
if (res)
|
||||
setLocalAdminFeedback({ rating: 'like' })
|
||||
},
|
||||
})}
|
||||
</Tooltip>
|
||||
<Tooltip selector={`user-feedback-${randomString(16)}`} content={t('appLog.detail.operation.dislike') as string}>
|
||||
{OperationBtn({
|
||||
innerContent: <IconWrapper><RatingIcon isLike={false} /></IconWrapper>,
|
||||
onClick: async () => {
|
||||
const res = await onFeedback?.(id, { rating: 'dislike' })
|
||||
if (res)
|
||||
setLocalAdminFeedback({ rating: 'dislike' })
|
||||
},
|
||||
})}
|
||||
</Tooltip>
|
||||
</>}
|
||||
</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${s.itemOperation} flex gap-2`}>
|
||||
{isWebScene ? userOperation() : adminOperation()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const getImgs = (list?: VisionFile[]) => {
|
||||
if (!list)
|
||||
return []
|
||||
return list.filter(file => file.type === 'image' && file.belongs_to === 'assistant')
|
||||
}
|
||||
|
||||
const agentModeAnswer = (
|
||||
<div>
|
||||
{agent_thoughts?.map((item, index) => (
|
||||
<div key={index}>
|
||||
{item.thought && (
|
||||
<Markdown content={item.thought} />
|
||||
)}
|
||||
{/* {item.tool} */}
|
||||
{/* perhaps not use tool */}
|
||||
{!!item.tool && (
|
||||
<Thought
|
||||
thought={item}
|
||||
allToolIcons={allToolIcons || {}}
|
||||
isFinished={!!item.observation || !isResponding}
|
||||
/>
|
||||
)}
|
||||
|
||||
{getImgs(item.message_files).length > 0 && (
|
||||
<ImageGallery srcs={getImgs(item.message_files).map(item => item.url)} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
const [containerWidth, setContainerWidth] = useState(0)
|
||||
const [contentWidth, setContentWidth] = useState(0)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const getContainerWidth = () => {
|
||||
if (containerRef.current)
|
||||
setContainerWidth(containerRef.current?.clientWidth + 24)
|
||||
}
|
||||
const getContentWidth = () => {
|
||||
if (contentRef.current)
|
||||
setContentWidth(contentRef.current?.clientWidth)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getContainerWidth()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isResponding)
|
||||
getContentWidth()
|
||||
}, [isResponding])
|
||||
|
||||
const operationWidth = useMemo(() => {
|
||||
let width = 0
|
||||
if (!item.isOpeningStatement)
|
||||
width += 28
|
||||
if (!item.isOpeningStatement && isShowPromptLog)
|
||||
width += 102 + 8
|
||||
if (!item.isOpeningStatement && isShowTextToSpeech)
|
||||
width += 33
|
||||
if (!item.isOpeningStatement && supportAnnotation)
|
||||
width += 96 + 8
|
||||
if (!feedbackDisabled && !item.feedbackDisabled)
|
||||
width += 60 + 8
|
||||
if (!feedbackDisabled && localAdminFeedback?.rating && !item.isOpeningStatement)
|
||||
width += 60 + 8
|
||||
if (!feedbackDisabled && feedback?.rating && !item.isOpeningStatement)
|
||||
width += 28 + 8
|
||||
return width
|
||||
}, [item.isOpeningStatement, item.feedbackDisabled, isShowPromptLog, isShowTextToSpeech, supportAnnotation, feedbackDisabled, localAdminFeedback?.rating, feedback?.rating])
|
||||
|
||||
const positionRight = useMemo(() => operationWidth < containerWidth - contentWidth - 4, [operationWidth, containerWidth, contentWidth])
|
||||
|
||||
return (
|
||||
// data-id for debug the item message is right
|
||||
<div key={id} data-id={id}>
|
||||
<div className='flex items-start'>
|
||||
{
|
||||
answerIcon || (
|
||||
<div className={`${s.answerIcon} w-10 h-10 shrink-0`}>
|
||||
{isResponding
|
||||
&& <div className={s.typeingIcon}>
|
||||
<LoadingAnim type='avatar' />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div ref={containerRef} className={cn(s.answerWrapWrap, 'chat-answer-container')}>
|
||||
<div className={cn(s.answerWrap, 'group')}>
|
||||
<div ref={contentRef} 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'}>
|
||||
{(isResponding && (isAgentMode ? (!content && (agent_thoughts || []).filter(item => !!item.thought || !!item.tool).length === 0) : !content))
|
||||
? (
|
||||
<div className='flex items-center justify-center w-6 h-5'>
|
||||
<LoadingAnim type='text' />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div>
|
||||
{annotation?.logAnnotation && (
|
||||
<div className='mb-1'>
|
||||
<div className='mb-3'>
|
||||
{isAgentMode
|
||||
? (<div className='line-through !text-gray-400'>{agentModeAnswer}</div>)
|
||||
: (
|
||||
<Markdown className='line-through !text-gray-400' content={content} />
|
||||
)}
|
||||
</div>
|
||||
<EditTitle title={t('appAnnotation.editBy', {
|
||||
author: annotation?.logAnnotation.account?.name,
|
||||
})} />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{annotation?.logAnnotation
|
||||
? (
|
||||
<Markdown content={annotation?.logAnnotation.content || ''} />
|
||||
)
|
||||
: (isAgentMode
|
||||
? agentModeAnswer
|
||||
: (
|
||||
<Markdown content={content} />
|
||||
))}
|
||||
</div>
|
||||
{(hasAnnotation && !annotation?.logAnnotation) && (
|
||||
<EditTitle className='mt-1' title={t('appAnnotation.editBy', {
|
||||
author: annotation?.authorName,
|
||||
})} />
|
||||
)}
|
||||
{item.isOpeningStatement && item.suggestedQuestions && item.suggestedQuestions.filter(q => !!q && q.trim()).length > 0 && (
|
||||
<div className='flex flex-wrap'>
|
||||
{item.suggestedQuestions.filter(q => !!q && q.trim()).map((question, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className='mt-1 mr-1 max-w-full last:mr-0 shrink-0 py-[5px] leading-[18px] items-center px-4 rounded-lg border border-gray-200 shadow-xs bg-white text-xs font-medium text-primary-600 cursor-pointer'
|
||||
onClick={() => onQueryChange(question)}
|
||||
>
|
||||
{question}
|
||||
</div>),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
!!citation?.length && isShowCitation && !isResponding && (
|
||||
<Citation data={citation} showHitInfo={isShowCitationHitInfo} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{hasAnnotation && (
|
||||
<div
|
||||
className={cn(s.hasAnnotationBtn, 'absolute -top-3.5 -right-3.5 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>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
'absolute -top-3.5 flex justify-end gap-1',
|
||||
positionRight ? '!top-[9px]' : '-right-3.5',
|
||||
)}
|
||||
style={positionRight ? { left: contentWidth + 8 } : {}}
|
||||
>
|
||||
{!item.isOpeningStatement && (
|
||||
<CopyBtn
|
||||
value={content}
|
||||
className={cn(s.copyBtn, 'mr-1')}
|
||||
/>
|
||||
)}
|
||||
{((isShowPromptLog && !isResponding) || (!item.isOpeningStatement && isShowTextToSpeech)) && (
|
||||
<div className='hidden group-hover:flex items-center w-max h-[28px] p-0.5 rounded-lg bg-white border-[0.5px] border-gray-100 shadow-md shrink-0'>
|
||||
{isShowPromptLog && !isResponding && (
|
||||
<Log logItem={item} />
|
||||
)}
|
||||
{!item.isOpeningStatement && isShowTextToSpeech && (
|
||||
<>
|
||||
<div className='mx-1 w-[1px] h-[14px] bg-gray-200'/>
|
||||
<AudioBtn
|
||||
value={content}
|
||||
noCache={false}
|
||||
className={cn(s.playBtn)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(!item.isOpeningStatement && supportAnnotation) && (
|
||||
<AnnotationCtrlBtn
|
||||
appId={appId!}
|
||||
messageId={id}
|
||||
annotationId={annotation?.id || ''}
|
||||
className={cn(s.annotationBtn, 'ml-1 shrink-0')}
|
||||
cached={hasAnnotation}
|
||||
query={question}
|
||||
answer={content}
|
||||
onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content, index)}
|
||||
onEdit={() => setIsShowReplyModal(true)}
|
||||
onRemoved={() => onAnnotationRemoved!(index)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<EditReplyModal
|
||||
isShow={isShowReplyModal}
|
||||
onHide={() => setIsShowReplyModal(false)}
|
||||
query={question}
|
||||
answer={content}
|
||||
onEdited={(editedQuery, editedAnswer) => onAnnotationEdited!(editedQuery, editedAnswer, index)}
|
||||
onAdded={(annotationId, authorName, editedQuery, editedAnswer) => onAnnotationAdded!(annotationId, authorName, editedQuery, editedAnswer, index)}
|
||||
appId={appId!}
|
||||
messageId={id}
|
||||
annotationId={annotation?.id || ''}
|
||||
createdAt={annotation?.created_at}
|
||||
onRemove={() => { }}
|
||||
/>
|
||||
|
||||
{!feedbackDisabled && !item.feedbackDisabled && renderItemOperation(displayScene !== 'console')}
|
||||
{/* Admin feedback is displayed only in the background. */}
|
||||
{!feedbackDisabled && renderFeedbackRating(localAdminFeedback?.rating, false, false)}
|
||||
{/* User feedback must be displayed */}
|
||||
{!feedbackDisabled && renderFeedbackRating(feedback?.rating, !isHideFeedbackEdit, displayScene !== 'console')}
|
||||
</div>
|
||||
</div>
|
||||
{more && <MoreInfo className='invisible group-hover:visible' more={more} isQuestion={false} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Answer)
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import type { CitationItem } from '../type'
|
||||
import Popup from './popup'
|
||||
|
||||
export type Resources = {
|
||||
documentId: string
|
||||
documentName: string
|
||||
dataSourceType: string
|
||||
sources: CitationItem[]
|
||||
}
|
||||
|
||||
type CitationProps = {
|
||||
data: CitationItem[]
|
||||
showHitInfo?: boolean
|
||||
containerClassName?: string
|
||||
}
|
||||
const Citation: FC<CitationProps> = ({
|
||||
data,
|
||||
showHitInfo,
|
||||
containerClassName = 'chat-answer-container',
|
||||
}) => {
|
||||
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(`.${containerClassName}`)!.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}`
|
||||
: <RiArrowDownSLine className='w-4 h-4 text-gray-600 rotate-180' />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Citation
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
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 !== 'notion'
|
||||
? (/\.([^.]*)$/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='shrink-0 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='shrink-0 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'>
|
||||
<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 || index + 1}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
showHitInfo && (
|
||||
<Link
|
||||
href={`/datasets/${source.dataset_id}/documents/${source.document_id}`}
|
||||
className='hidden items-center h-[18px] text-xs text-primary-600 group-hover:flex'>
|
||||
{t('common.chat.citation.linkToDataset')}
|
||||
<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' />}
|
||||
/>
|
||||
{
|
||||
source.score && (
|
||||
<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
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
'use client'
|
||||
import { useRef, useState } from 'react'
|
||||
import { t } from 'i18next'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import s from './style.module.css'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { randomString } from '@/utils'
|
||||
|
||||
type ICopyBtnProps = {
|
||||
value: string
|
||||
className?: string
|
||||
isPlain?: boolean
|
||||
}
|
||||
|
||||
const CopyBtn = ({
|
||||
value,
|
||||
className,
|
||||
isPlain,
|
||||
}: ICopyBtnProps) => {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
const selector = useRef(`copy-tooltip-${randomString(4)}`)
|
||||
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<Tooltip
|
||||
selector={selector.current}
|
||||
content={(isCopied ? t('appApi.copied') : t('appApi.copy')) as string}
|
||||
className='z-10'
|
||||
>
|
||||
<div
|
||||
className={'box-border p-0.5 flex items-center justify-center rounded-md bg-white cursor-pointer'}
|
||||
style={!isPlain
|
||||
? {
|
||||
boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
|
||||
}
|
||||
: {}}
|
||||
onClick={() => {
|
||||
copy(value)
|
||||
setIsCopied(true)
|
||||
}}
|
||||
>
|
||||
<div className={`w-6 h-6 rounded-md hover:bg-gray-50 ${s.copyIcon} ${isCopied ? s.copied : ''}`}></div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CopyBtn
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
.copyIcon {
|
||||
background-image: url(~@/app/components/develop/secret-key/assets/copy.svg);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.copyIcon:hover {
|
||||
background-image: url(~@/app/components/develop/secret-key/assets/copy-hover.svg);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.copyIcon.copied {
|
||||
background-image: url(~@/app/components/develop/secret-key/assets/copied.svg);
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import type { FC, SVGProps } from 'react'
|
||||
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
export const stopIcon = (
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M7.00004 0.583313C3.45621 0.583313 0.583374 3.45615 0.583374 6.99998C0.583374 10.5438 3.45621 13.4166 7.00004 13.4166C10.5439 13.4166 13.4167 10.5438 13.4167 6.99998C13.4167 3.45615 10.5439 0.583313 7.00004 0.583313ZM4.73029 4.98515C4.66671 5.10993 4.66671 5.27328 4.66671 5.59998V8.39998C4.66671 8.72668 4.66671 8.89003 4.73029 9.01481C4.78621 9.12457 4.87545 9.21381 4.98521 9.26973C5.10999 9.33331 5.27334 9.33331 5.60004 9.33331H8.40004C8.72674 9.33331 8.89009 9.33331 9.01487 9.26973C9.12463 9.21381 9.21387 9.12457 9.2698 9.01481C9.33337 8.89003 9.33337 8.72668 9.33337 8.39998V5.59998C9.33337 5.27328 9.33337 5.10993 9.2698 4.98515C9.21387 4.87539 9.12463 4.78615 9.01487 4.73023C8.89009 4.66665 8.72674 4.66665 8.40004 4.66665H5.60004C5.27334 4.66665 5.10999 4.66665 4.98521 4.73023C4.87545 4.78615 4.78621 4.87539 4.73029 4.98515Z" fill="#667085" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const OpeningStatementIcon = ({ className }: SVGProps<SVGElement>) => (
|
||||
<svg className={className} width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M6.25002 1C3.62667 1 1.50002 3.12665 1.50002 5.75C1.50002 6.28 1.58702 6.79071 1.7479 7.26801C1.7762 7.35196 1.79285 7.40164 1.80368 7.43828L1.80722 7.45061L1.80535 7.45452C1.79249 7.48102 1.77339 7.51661 1.73766 7.58274L0.911727 9.11152C0.860537 9.20622 0.807123 9.30503 0.770392 9.39095C0.733879 9.47635 0.674738 9.63304 0.703838 9.81878C0.737949 10.0365 0.866092 10.2282 1.05423 10.343C1.21474 10.4409 1.38213 10.4461 1.475 10.4451C1.56844 10.444 1.68015 10.4324 1.78723 10.4213L4.36472 10.1549C4.406 10.1506 4.42758 10.1484 4.44339 10.1472L4.44542 10.147L4.45161 10.1492C4.47103 10.1562 4.49738 10.1663 4.54285 10.1838C5.07332 10.3882 5.64921 10.5 6.25002 10.5C8.87338 10.5 11 8.37335 11 5.75C11 3.12665 8.87338 1 6.25002 1ZM4.48481 4.29111C5.04844 3.81548 5.7986 3.9552 6.24846 4.47463C6.69831 3.9552 7.43879 3.82048 8.01211 4.29111C8.58544 4.76175 8.6551 5.562 8.21247 6.12453C7.93825 6.47305 7.24997 7.10957 6.76594 7.54348C6.58814 7.70286 6.49924 7.78255 6.39255 7.81466C6.30103 7.84221 6.19589 7.84221 6.10436 7.81466C5.99767 7.78255 5.90878 7.70286 5.73098 7.54348C5.24694 7.10957 4.55867 6.47305 4.28444 6.12453C3.84182 5.562 3.92117 4.76675 4.48481 4.29111Z" fill="#667085" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const RatingIcon: FC<{ isLike: boolean }> = ({ isLike }) => {
|
||||
return isLike ? <HandThumbUpIcon className='w-4 h-4' /> : <HandThumbDownIcon className='w-4 h-4' />
|
||||
}
|
||||
|
||||
export const EditIcon = ({ 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="M14 11.9998L13.3332 12.7292C12.9796 13.1159 12.5001 13.3332 12.0001 13.3332C11.5001 13.3332 11.0205 13.1159 10.6669 12.7292C10.3128 12.3432 9.83332 12.1265 9.33345 12.1265C8.83359 12.1265 8.35409 12.3432 7.99998 12.7292M2 13.3332H3.11636C3.44248 13.3332 3.60554 13.3332 3.75899 13.2963C3.89504 13.2637 4.0251 13.2098 4.1444 13.1367C4.27895 13.0542 4.39425 12.9389 4.62486 12.7083L13 4.33316C13.5523 3.78087 13.5523 2.88544 13 2.33316C12.4477 1.78087 11.5523 1.78087 11 2.33316L2.62484 10.7083C2.39424 10.9389 2.27894 11.0542 2.19648 11.1888C2.12338 11.3081 2.0695 11.4381 2.03684 11.5742C2 11.7276 2 11.8907 2 12.2168V13.3332Z" stroke="#6B7280" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const EditIconSolid = ({ className }: SVGProps<SVGElement>) => {
|
||||
return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M10.8374 8.63108C11.0412 8.81739 11.0554 9.13366 10.8691 9.33747L10.369 9.88449C10.0142 10.2725 9.52293 10.5001 9.00011 10.5001C8.47746 10.5001 7.98634 10.2727 7.63157 9.8849C7.45561 9.69325 7.22747 9.59515 7.00014 9.59515C6.77271 9.59515 6.54446 9.69335 6.36846 9.88517C6.18177 10.0886 5.86548 10.1023 5.66201 9.91556C5.45853 9.72888 5.44493 9.41259 5.63161 9.20911C5.98678 8.82201 6.47777 8.59515 7.00014 8.59515C7.52251 8.59515 8.0135 8.82201 8.36867 9.20911L8.36924 9.20974C8.54486 9.4018 8.77291 9.50012 9.00011 9.50012C9.2273 9.50012 9.45533 9.40182 9.63095 9.20979L10.131 8.66276C10.3173 8.45895 10.6336 8.44476 10.8374 8.63108Z" fill="#6B7280" />
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M7.89651 1.39656C8.50599 0.787085 9.49414 0.787084 10.1036 1.39656C10.7131 2.00604 10.7131 2.99419 10.1036 3.60367L3.82225 9.88504C3.81235 9.89494 3.80254 9.90476 3.79281 9.91451C3.64909 10.0585 3.52237 10.1855 3.3696 10.2791C3.23539 10.3613 3.08907 10.4219 2.93602 10.4587C2.7618 10.5005 2.58242 10.5003 2.37897 10.5001C2.3652 10.5001 2.35132 10.5001 2.33732 10.5001H1.50005C1.22391 10.5001 1.00005 10.2763 1.00005 10.0001V9.16286C1.00005 9.14886 1.00004 9.13497 1.00003 9.1212C0.999836 8.91776 0.999669 8.73838 1.0415 8.56416C1.07824 8.4111 1.13885 8.26479 1.22109 8.13058C1.31471 7.97781 1.44166 7.85109 1.58566 7.70736C1.5954 7.69764 1.60523 7.68783 1.61513 7.67793L7.89651 1.39656Z" fill="#6B7280" />
|
||||
</svg>
|
||||
}
|
||||
|
||||
export const TryToAskIcon = (
|
||||
<svg width="11" height="10" viewBox="0 0 11 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.88889 0.683718C5.827 0.522805 5.67241 0.416626 5.5 0.416626C5.3276 0.416626 5.173 0.522805 5.11111 0.683718L4.27279 2.86334C4.14762 3.18877 4.10829 3.28255 4.05449 3.35821C4.00051 3.43413 3.93418 3.50047 3.85826 3.55445C3.78259 3.60825 3.68881 3.64758 3.36338 3.77275L1.18376 4.61106C1.02285 4.67295 0.916668 4.82755 0.916668 4.99996C0.916668 5.17236 1.02285 5.32696 1.18376 5.38885L3.36338 6.22717C3.68881 6.35234 3.78259 6.39167 3.85826 6.44547C3.93418 6.49945 4.00051 6.56578 4.05449 6.6417C4.10829 6.71737 4.14762 6.81115 4.27279 7.13658L5.11111 9.3162C5.173 9.47711 5.3276 9.58329 5.5 9.58329C5.67241 9.58329 5.82701 9.47711 5.8889 9.3162L6.72721 7.13658C6.85238 6.81115 6.89171 6.71737 6.94551 6.6417C6.99949 6.56578 7.06583 6.49945 7.14175 6.44547C7.21741 6.39167 7.31119 6.35234 7.63662 6.22717L9.81624 5.38885C9.97715 5.32696 10.0833 5.17236 10.0833 4.99996C10.0833 4.82755 9.97715 4.67295 9.81624 4.61106L7.63662 3.77275C7.31119 3.64758 7.21741 3.60825 7.14175 3.55445C7.06583 3.50047 6.99949 3.43413 6.94551 3.35821C6.89171 3.28255 6.85238 3.18877 6.72721 2.86334L5.88889 0.683718Z" fill="#667085" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
export const ReplayIcon = ({ className }: SVGProps<SVGElement>) => (
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<path d="M1.33301 6.66667C1.33301 6.66667 2.66966 4.84548 3.75556 3.75883C4.84147 2.67218 6.34207 2 7.99967 2C11.3134 2 13.9997 4.68629 13.9997 8C13.9997 11.3137 11.3134 14 7.99967 14C5.26428 14 2.95642 12.1695 2.23419 9.66667M1.33301 6.66667V2.66667M1.33301 6.66667H5.33301" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.03647 1.5547C0.59343 0.890144 1.06982 0 1.86852 0H8V12L1.03647 1.5547Z" fill="#F3F4F6"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 202 B |
|
Before Width: | Height: | Size: 2.1 KiB |
|
|
@ -1,3 +0,0 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 11.9998L13.3332 12.7292C12.9796 13.1159 12.5001 13.3332 12.0001 13.3332C11.5001 13.3332 11.0205 13.1159 10.6669 12.7292C10.3128 12.3432 9.83332 12.1265 9.33345 12.1265C8.83359 12.1265 8.35409 12.3432 7.99998 12.7292M2 13.3332H3.11636C3.44248 13.3332 3.60554 13.3332 3.75899 13.2963C3.89504 13.2637 4.0251 13.2098 4.1444 13.1367C4.27895 13.0542 4.39425 12.9389 4.62486 12.7083L13 4.33316C13.5523 3.78087 13.5523 2.88544 13 2.33316C12.4477 1.78087 11.5523 1.78087 11 2.33316L2.62484 10.7083C2.39424 10.9389 2.27894 11.0542 2.19648 11.1888C2.12338 11.3081 2.0695 11.4381 2.03684 11.5742C2 11.7276 2 11.8907 2 12.2168V13.3332Z" stroke="#6B7280" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 804 B |
|
|
@ -1,3 +0,0 @@
|
|||
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.96353 1.5547C7.40657 0.890144 6.93018 0 6.13148 0H0V12L6.96353 1.5547Z" fill="#E1EFFE"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 202 B |
|
Before Width: | Height: | Size: 36 KiB |
|
|
@ -1,3 +0,0 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.447 16.8939C23.6128 16.8108 23.7523 16.6832 23.8498 16.5253C23.9473 16.3674 23.9989 16.1855 23.9989 15.9999C23.9989 15.8144 23.9473 15.6325 23.8498 15.4746C23.7523 15.3167 23.6128 15.1891 23.447 15.1059L9.44697 8.10595C9.27338 8.01909 9.07827 7.98463 8.88543 8.00677C8.69259 8.02891 8.51036 8.1067 8.36098 8.23064C8.2116 8.35458 8.10151 8.51931 8.04415 8.70475C7.9868 8.89019 7.98465 9.08831 8.03797 9.27495L9.46697 14.2749C9.52674 14.4839 9.65297 14.6677 9.82655 14.7985C10.0001 14.9294 10.2116 15.0001 10.429 14.9999H15C15.2652 14.9999 15.5195 15.1053 15.7071 15.2928C15.8946 15.4804 16 15.7347 16 15.9999C16 16.2652 15.8946 16.5195 15.7071 16.7071C15.5195 16.8946 15.2652 16.9999 15 16.9999H10.429C10.2116 16.9998 10.0001 17.0705 9.82655 17.2013C9.65297 17.3322 9.52674 17.516 9.46697 17.7249L8.03897 22.7249C7.98554 22.9115 7.98756 23.1096 8.04478 23.2951C8.10201 23.4805 8.21195 23.6453 8.36122 23.7693C8.51049 23.8934 8.69263 23.9713 8.88542 23.9936C9.07821 24.0159 9.27332 23.9816 9.44697 23.8949L23.447 16.8949V16.8939Z" fill="#1C64F2"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
|
@ -1,3 +0,0 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.447 16.8939C23.6128 16.8108 23.7523 16.6832 23.8498 16.5253C23.9473 16.3674 23.9989 16.1855 23.9989 15.9999C23.9989 15.8144 23.9473 15.6325 23.8498 15.4746C23.7523 15.3167 23.6128 15.1891 23.447 15.1059L9.44697 8.10595C9.27338 8.01909 9.07827 7.98463 8.88543 8.00677C8.69259 8.02891 8.51036 8.1067 8.36098 8.23064C8.2116 8.35458 8.10151 8.51931 8.04415 8.70475C7.9868 8.89019 7.98465 9.08831 8.03797 9.27495L9.46697 14.2749C9.52674 14.4839 9.65297 14.6677 9.82655 14.7985C10.0001 14.9294 10.2116 15.0001 10.429 14.9999H15C15.2652 14.9999 15.5195 15.1053 15.7071 15.2928C15.8946 15.4804 16 15.7347 16 15.9999C16 16.2652 15.8946 16.5195 15.7071 16.7071C15.5195 16.8946 15.2652 16.9999 15 16.9999H10.429C10.2116 16.9998 10.0001 17.0705 9.82655 17.2013C9.65297 17.3322 9.52674 17.516 9.46697 17.7249L8.03897 22.7249C7.98554 22.9115 7.98756 23.1096 8.04478 23.2951C8.10201 23.4805 8.21195 23.6453 8.36122 23.7693C8.51049 23.8934 8.69263 23.9713 8.88542 23.9936C9.07821 24.0159 9.27332 23.9816 9.44697 23.8949L23.447 16.8949V16.8939Z" fill="#D1D5DB"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
|
@ -1,19 +0,0 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_2358_1380)">
|
||||
<rect x="2" y="1" width="16" height="16" rx="8" fill="white"/>
|
||||
<path opacity="0.7" d="M13.5 9H13.505M14 9C14 9.13261 13.9473 9.25979 13.8536 9.35355C13.7598 9.44732 13.6326 9.5 13.5 9.5C13.3674 9.5 13.2402 9.44732 13.1464 9.35355C13.0527 9.25979 13 9.13261 13 9C13 8.86739 13.0527 8.74021 13.1464 8.64645C13.2402 8.55268 13.3674 8.5 13.5 8.5C13.6326 8.5 13.7598 8.55268 13.8536 8.64645C13.9473 8.74021 14 8.86739 14 9Z" stroke="#155EEF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path opacity="0.6" d="M10 9H10.005M10.5 9C10.5 9.13261 10.4473 9.25979 10.3536 9.35355C10.2598 9.44732 10.1326 9.5 10 9.5C9.86739 9.5 9.74021 9.44732 9.64645 9.35355C9.55268 9.25979 9.5 9.13261 9.5 9C9.5 8.86739 9.55268 8.74021 9.64645 8.64645C9.74021 8.55268 9.86739 8.5 10 8.5C10.1326 8.5 10.2598 8.55268 10.3536 8.64645C10.4473 8.74021 10.5 8.86739 10.5 9Z" stroke="#155EEF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path opacity="0.3" d="M6.5 9H6.505M7 9C7 9.13261 6.94732 9.25979 6.85355 9.35355C6.75979 9.44732 6.63261 9.5 6.5 9.5C6.36739 9.5 6.24021 9.44732 6.14645 9.35355C6.05268 9.25979 6 9.13261 6 9C6 8.86739 6.05268 8.74021 6.14645 8.64645C6.24021 8.55268 6.36739 8.5 6.5 8.5C6.63261 8.5 6.75979 8.55268 6.85355 8.64645C6.94732 8.74021 7 8.86739 7 9Z" stroke="#155EEF" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_2358_1380" x="0" y="0" width="20" height="20" 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_2358_1380"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2358_1380" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
|
@ -1,455 +0,0 @@
|
|||
'use client'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import Textarea from 'rc-textarea'
|
||||
import { useContext } from 'use-context-selector'
|
||||
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 } from './type'
|
||||
import { TryToAskIcon, stopIcon } from './icon-component'
|
||||
import Answer from './answer'
|
||||
import Question from './question'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import Button from '@/app/components/base/button'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import VoiceInput from '@/app/components/base/voice-input'
|
||||
import { Microphone01 } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { Microphone01 as Microphone01Solid } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||
import { XCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import ChatImageUploader from '@/app/components/base/image-uploader/chat-image-uploader'
|
||||
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'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
|
||||
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
|
||||
*/
|
||||
feedbackDisabled?: boolean
|
||||
/**
|
||||
* Whether to display the input area
|
||||
*/
|
||||
isHideFeedbackEdit?: boolean
|
||||
isHideSendInput?: boolean
|
||||
onFeedback?: FeedbackFunc
|
||||
checkCanSend?: () => boolean
|
||||
query?: string
|
||||
onQueryChange?: (query: string) => void
|
||||
onSend?: (message: string, files: VisionFile[]) => void
|
||||
displayScene?: DisplayScene
|
||||
useCurrentUserAvatar?: boolean
|
||||
isResponding?: boolean
|
||||
canStopResponding?: boolean
|
||||
abortResponding?: () => void
|
||||
controlClearQuery?: number
|
||||
controlFocus?: number
|
||||
isShowSuggestion?: boolean
|
||||
suggestionList?: string[]
|
||||
isShowSpeechToText?: boolean
|
||||
isShowTextToSpeech?: boolean
|
||||
isShowCitation?: boolean
|
||||
answerIcon?: ReactNode
|
||||
isShowConfigElem?: boolean
|
||||
dataSets?: DataSet[]
|
||||
isShowCitationHitInfo?: boolean
|
||||
isShowPromptLog?: boolean
|
||||
visionConfig?: VisionSettings
|
||||
supportAnnotation?: boolean
|
||||
allToolIcons?: Record<string, string | Emoji>
|
||||
customDisclaimer?: string
|
||||
}
|
||||
|
||||
const Chat: FC<IChatProps> = ({
|
||||
configElem,
|
||||
chatList,
|
||||
query = '',
|
||||
onQueryChange = () => { },
|
||||
feedbackDisabled = false,
|
||||
isHideFeedbackEdit = false,
|
||||
isHideSendInput = false,
|
||||
onFeedback,
|
||||
checkCanSend,
|
||||
onSend = () => { },
|
||||
displayScene,
|
||||
useCurrentUserAvatar,
|
||||
isResponding,
|
||||
canStopResponding,
|
||||
abortResponding,
|
||||
controlClearQuery,
|
||||
controlFocus,
|
||||
isShowSuggestion,
|
||||
suggestionList,
|
||||
isShowSpeechToText,
|
||||
isShowTextToSpeech,
|
||||
isShowCitation,
|
||||
answerIcon,
|
||||
isShowConfigElem,
|
||||
dataSets,
|
||||
isShowCitationHitInfo,
|
||||
isShowPromptLog,
|
||||
visionConfig,
|
||||
appId,
|
||||
supportAnnotation,
|
||||
onChatListChange,
|
||||
allToolIcons,
|
||||
customDisclaimer,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const {
|
||||
files,
|
||||
onUpload,
|
||||
onRemove,
|
||||
onReUpload,
|
||||
onImageLinkLoadError,
|
||||
onImageLinkLoadSuccess,
|
||||
onClear,
|
||||
} = useImageFiles()
|
||||
const { onPaste } = useClipboardUploader({ onUpload, visionConfig, files })
|
||||
const { onDragEnter, onDragLeave, onDragOver, onDrop, isDragActive } = useDraggableUploader<HTMLTextAreaElement>({ onUpload, files, visionConfig })
|
||||
const isUseInputMethod = useRef(false)
|
||||
|
||||
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const value = e.target.value
|
||||
onQueryChange(value)
|
||||
}
|
||||
|
||||
const logError = (message: string) => {
|
||||
notify({ type: 'error', message, duration: 3000 })
|
||||
}
|
||||
|
||||
const valid = (q?: string) => {
|
||||
const sendQuery = q || query
|
||||
if (!sendQuery || sendQuery.trim() === '') {
|
||||
logError('Message cannot be empty')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (controlClearQuery)
|
||||
onQueryChange('')
|
||||
}, [controlClearQuery])
|
||||
|
||||
const handleSend = (q?: string) => {
|
||||
if (!valid(q) || (checkCanSend && !checkCanSend()))
|
||||
return
|
||||
onSend(q || query, files.filter(file => file.progress !== -1).map(fileItem => ({
|
||||
type: 'image',
|
||||
transfer_method: fileItem.type,
|
||||
url: fileItem.url,
|
||||
upload_file_id: fileItem.fileId,
|
||||
})))
|
||||
if (!files.find(item => item.type === TransferMethod.local_file && !item.fileId)) {
|
||||
if (files.length)
|
||||
onClear()
|
||||
if (!isResponding)
|
||||
onQueryChange('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyUp = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.code === 'Enter') {
|
||||
e.preventDefault()
|
||||
// prevent send message when using input method enter
|
||||
if (!e.shiftKey && !isUseInputMethod.current)
|
||||
handleSend()
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
isUseInputMethod.current = e.nativeEvent.isComposing
|
||||
if (e.code === 'Enter' && !e.shiftKey) {
|
||||
onQueryChange(query.replace(/\n$/, ''))
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
const sendBtn = <div className={cn(!(!query || query.trim() === '') && s.sendBtnActive, `${s.sendBtn} w-8 h-8 cursor-pointer rounded-md`)} onClick={() => handleSend()}></div>
|
||||
|
||||
const suggestionListRef = useRef<HTMLDivElement>(null)
|
||||
const [hasScrollbar, setHasScrollbar] = useState(false)
|
||||
useLayoutEffect(() => {
|
||||
if (suggestionListRef.current) {
|
||||
const listDom = suggestionListRef.current
|
||||
const hasScrollbar = listDom.scrollWidth > listDom.clientWidth
|
||||
setHasScrollbar(hasScrollbar)
|
||||
}
|
||||
}, [suggestionList])
|
||||
|
||||
const [voiceInputShow, setVoiceInputShow] = useState(false)
|
||||
const handleVoiceInputShow = () => {
|
||||
(Recorder as any).getPermission().then(() => {
|
||||
setVoiceInputShow(true)
|
||||
}, () => {
|
||||
logError(t('common.voiceInput.notAllow'))
|
||||
})
|
||||
}
|
||||
const handleQueryChangeFromAnswer = useCallback((val: string) => {
|
||||
onQueryChange(val)
|
||||
handleSend(val)
|
||||
}, [])
|
||||
const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
|
||||
onChatListChange?.(chatList.map((item, i) => {
|
||||
if (i === index - 1) {
|
||||
return {
|
||||
...item,
|
||||
content: query,
|
||||
}
|
||||
}
|
||||
if (i === index) {
|
||||
return {
|
||||
...item,
|
||||
annotation: {
|
||||
...item.annotation,
|
||||
logAnnotation: {
|
||||
...item.annotation?.logAnnotation,
|
||||
content: answer,
|
||||
},
|
||||
} as any,
|
||||
}
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}, [chatList])
|
||||
const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => {
|
||||
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
|
||||
}))
|
||||
}, [chatList])
|
||||
const handleAnnotationRemoved = useCallback((index: number) => {
|
||||
onChatListChange?.(chatList.map((item, i) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
...item,
|
||||
content: item.content,
|
||||
annotation: undefined,
|
||||
}
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}, [chatList])
|
||||
|
||||
return (
|
||||
<div className={cn('px-3.5', 'h-full')}>
|
||||
{isShowConfigElem && (configElem || null)}
|
||||
{/* Chat List */}
|
||||
<div className={cn((isShowConfigElem && configElem) ? 'h-0' : 'h-full', 'space-y-[30px]')}>
|
||||
{chatList.map((item, index) => {
|
||||
if (item.isAnswer) {
|
||||
const isLast = item.id === chatList[chatList.length - 1].id
|
||||
const citation = item.citation
|
||||
return <Answer
|
||||
key={item.id}
|
||||
item={item}
|
||||
index={index}
|
||||
onQueryChange={handleQueryChangeFromAnswer}
|
||||
feedbackDisabled={feedbackDisabled}
|
||||
isHideFeedbackEdit={isHideFeedbackEdit}
|
||||
onFeedback={onFeedback}
|
||||
displayScene={displayScene ?? 'web'}
|
||||
isResponding={isResponding && isLast}
|
||||
answerIcon={answerIcon}
|
||||
citation={citation}
|
||||
dataSets={dataSets}
|
||||
isShowCitation={isShowCitation}
|
||||
isShowCitationHitInfo={isShowCitationHitInfo}
|
||||
isShowTextToSpeech={isShowTextToSpeech}
|
||||
supportAnnotation={supportAnnotation}
|
||||
appId={appId}
|
||||
question={chatList[index - 1]?.content}
|
||||
onAnnotationEdited={handleAnnotationEdited}
|
||||
onAnnotationAdded={handleAnnotationAdded}
|
||||
onAnnotationRemoved={handleAnnotationRemoved}
|
||||
allToolIcons={allToolIcons}
|
||||
isShowPromptLog={isShowPromptLog}
|
||||
/>
|
||||
}
|
||||
return (
|
||||
<Question
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
content={item.content}
|
||||
more={item.more}
|
||||
useCurrentUserAvatar={useCurrentUserAvatar}
|
||||
item={item}
|
||||
isShowPromptLog={isShowPromptLog}
|
||||
isResponding={isResponding}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{!isHideSendInput && (
|
||||
<div className={cn(!feedbackDisabled && '!left-3.5 !right-3.5', 'absolute z-10 bottom-0 left-0 right-0')}>
|
||||
{/* Thinking is sync and can not be stopped */}
|
||||
{(isResponding && canStopResponding && ((!!chatList[chatList.length - 1]?.content) || (chatList[chatList.length - 1]?.agent_thoughts && chatList[chatList.length - 1].agent_thoughts!.length > 0))) && (
|
||||
<div className='flex justify-center mb-4'>
|
||||
<Button className='flex items-center space-x-1 bg-white' onClick={() => abortResponding?.()}>
|
||||
{stopIcon}
|
||||
<span className='text-xs text-gray-500 font-normal'>{t('appDebug.operation.stopResponding')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isShowSuggestion && (
|
||||
<div className='pt-2'>
|
||||
<div className='flex items-center justify-center mb-2.5'>
|
||||
<div className='grow h-[1px]'
|
||||
style={{
|
||||
background: 'linear-gradient(270deg, #F3F4F6 0%, rgba(243, 244, 246, 0) 100%)',
|
||||
}}></div>
|
||||
<div className='shrink-0 flex items-center px-3 space-x-1'>
|
||||
{TryToAskIcon}
|
||||
<span className='text-xs text-gray-500 font-medium'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.tryToAsk')}</span>
|
||||
</div>
|
||||
<div className='grow h-[1px]'
|
||||
style={{
|
||||
background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)',
|
||||
}}></div>
|
||||
</div>
|
||||
{/* has scrollbar would hide part of first item */}
|
||||
<div ref={suggestionListRef} className={cn(!hasScrollbar && 'justify-center', 'flex overflow-x-auto pb-2')}>
|
||||
{suggestionList?.map((item, index) => (
|
||||
<div key={item} className='shrink-0 flex justify-center mr-2'>
|
||||
<Button
|
||||
key={index}
|
||||
onClick={() => onQueryChange(item)}
|
||||
>
|
||||
<span className='text-primary-600 text-xs font-medium'>{item}</span>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='relative'>
|
||||
<div className={cn('relative p-[5.5px] max-h-[150px] bg-white border-[1.5px] border-gray-200 rounded-xl overflow-y-auto', isDragActive && 'border-primary-600')}>
|
||||
{visionConfig?.enabled && (
|
||||
<>
|
||||
<div className='absolute bottom-2 left-2 flex items-center'>
|
||||
<ChatImageUploader
|
||||
settings={visionConfig}
|
||||
onUpload={onUpload}
|
||||
disabled={files.length >= visionConfig.number_limits}
|
||||
/>
|
||||
<div className='mx-1 w-[1px] h-4 bg-black/5' />
|
||||
</div>
|
||||
<div className='pl-[52px]'>
|
||||
<ImageList
|
||||
list={files}
|
||||
onRemove={onRemove}
|
||||
onReUpload={onReUpload}
|
||||
onImageLinkLoadSuccess={onImageLinkLoadSuccess}
|
||||
onImageLinkLoadError={onImageLinkLoadError}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Textarea
|
||||
className={`
|
||||
block w-full px-2 pr-[118px] py-[7px] leading-5 max-h-none text-sm text-gray-700 outline-none appearance-none resize-none
|
||||
${visionConfig?.enabled && 'pl-12'}
|
||||
`}
|
||||
value={query}
|
||||
onChange={handleContentChange}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={onPaste}
|
||||
onDragEnter={onDragEnter}
|
||||
onDragLeave={onDragLeave}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
autoSize
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute bottom-2 right-2 flex items-center h-8">
|
||||
<div className={`${s.count} mr-4 h-5 leading-5 text-sm bg-gray-50 text-gray-500`}>{query.trim().length}</div>
|
||||
{
|
||||
query
|
||||
? (
|
||||
<div className='flex justify-center items-center w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg' onClick={() => onQueryChange('')}>
|
||||
<XCircle className='w-4 h-4 text-[#98A2B3]' />
|
||||
</div>
|
||||
)
|
||||
: isShowSpeechToText
|
||||
? (
|
||||
<div
|
||||
className='group flex justify-center items-center w-8 h-8 hover:bg-primary-50 rounded-lg cursor-pointer'
|
||||
onClick={handleVoiceInputShow}
|
||||
>
|
||||
<Microphone01 className='block w-4 h-4 text-gray-500 group-hover:hidden' />
|
||||
<Microphone01Solid className='hidden w-4 h-4 text-primary-600 group-hover:block' />
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
}
|
||||
<div className='mx-2 w-[1px] h-4 bg-black opacity-5' />
|
||||
{isMobile
|
||||
? sendBtn
|
||||
: (
|
||||
<TooltipPlus
|
||||
popupContent={
|
||||
<div>
|
||||
<div>{t('common.operation.send')} Enter</div>
|
||||
<div>{t('common.operation.lineBreak')} Shift Enter</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{sendBtn}
|
||||
</TooltipPlus>
|
||||
)}
|
||||
</div>
|
||||
{voiceInputShow && (
|
||||
<VoiceInput
|
||||
onCancel={() => setVoiceInputShow(false)}
|
||||
onConverted={text => onQueryChange(text)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{customDisclaimer && <div className='text-xs text-gray-500 mt-1 text-center'>
|
||||
{customDisclaimer}
|
||||
</div>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Chat)
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import s from './style.module.css'
|
||||
|
||||
export type ILoaidingAnimProps = {
|
||||
type: 'text' | 'avatar'
|
||||
}
|
||||
|
||||
const LoaidingAnim: FC<ILoaidingAnimProps> = ({
|
||||
type,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`${s['dot-flashing']} ${s[type]}`}></div>
|
||||
)
|
||||
}
|
||||
export default React.memo(LoaidingAnim)
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
.dot-flashing {
|
||||
position: relative;
|
||||
animation: 1s infinite linear alternate;
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
|
||||
.dot-flashing::before,
|
||||
.dot-flashing::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
animation: 1s infinite linear alternate;
|
||||
}
|
||||
|
||||
.dot-flashing::before {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.dot-flashing::after {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
@keyframes dot-flashing {
|
||||
0% {
|
||||
background-color: #667085;
|
||||
}
|
||||
|
||||
50%,
|
||||
100% {
|
||||
background-color: rgba(102, 112, 133, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dot-flashing-avatar {
|
||||
0% {
|
||||
background-color: #155EEF;
|
||||
}
|
||||
|
||||
50%,
|
||||
100% {
|
||||
background-color: rgba(21, 94, 239, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
.text,
|
||||
.text::before,
|
||||
.text::after {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background-color: #667085;
|
||||
color: #667085;
|
||||
animation-name: dot-flashing;
|
||||
}
|
||||
|
||||
.text::before {
|
||||
left: -7px;
|
||||
}
|
||||
|
||||
.text::after {
|
||||
left: 7px;
|
||||
}
|
||||
|
||||
.avatar,
|
||||
.avatar::before,
|
||||
.avatar::after {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
border-radius: 50%;
|
||||
background-color: #155EEF;
|
||||
color: #155EEF;
|
||||
animation-name: dot-flashing-avatar;
|
||||
}
|
||||
|
||||
.avatar::before {
|
||||
left: -5px;
|
||||
}
|
||||
|
||||
.avatar::after {
|
||||
left: 5px;
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { File02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import type { IChatItem } from '@/app/components/app/chat/type'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
|
||||
type LogProps = {
|
||||
logItem: IChatItem
|
||||
}
|
||||
const Log: FC<LogProps> = ({
|
||||
logItem,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const setCurrentLogItem = useAppStore(s => s.setCurrentLogItem)
|
||||
const setShowPromptLogModal = useAppStore(s => s.setShowPromptLogModal)
|
||||
const setShowAgentLogModal = useAppStore(s => s.setShowAgentLogModal)
|
||||
const setShowMessageLogModal = useAppStore(s => s.setShowMessageLogModal)
|
||||
const { workflow_run_id: runID, agent_thoughts } = logItem
|
||||
const isAgent = agent_thoughts && agent_thoughts.length > 0
|
||||
|
||||
return (
|
||||
<div
|
||||
className='shrink-0 p-1 flex items-center justify-center rounded-[6px] font-medium text-gray-500 hover:bg-gray-50 cursor-pointer hover:text-gray-700'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
setCurrentLogItem(logItem)
|
||||
if (runID)
|
||||
setShowMessageLogModal(true)
|
||||
else if (isAgent)
|
||||
setShowAgentLogModal(true)
|
||||
else
|
||||
setShowPromptLogModal(true)
|
||||
}}
|
||||
>
|
||||
<File02 className='mr-1 w-4 h-4' />
|
||||
<div className='text-xs leading-4'>{runID ? t('appLog.viewLog') : isAgent ? t('appLog.agentLog') : t('appLog.promptLog')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Log
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import mermaid from 'mermaid'
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
let mermaidAPI: any
|
||||
mermaidAPI = null
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'default',
|
||||
flowchart: {
|
||||
htmlLabels: true,
|
||||
useMaxWidth: true,
|
||||
},
|
||||
})
|
||||
mermaidAPI = mermaid.mermaidAPI
|
||||
}
|
||||
|
||||
const style = {
|
||||
minWidth: '480px',
|
||||
height: 'auto',
|
||||
overflow: 'auto',
|
||||
}
|
||||
|
||||
const Flowchart = React.forwardRef((props: {
|
||||
PrimitiveCode: string
|
||||
}, ref) => {
|
||||
const [svgCode, setSvgCode] = useState(null)
|
||||
const chartId = useRef(`flowchart_${CryptoJS.MD5(props.PrimitiveCode).toString()}`)
|
||||
const [isRender, setIsRender] = useState(true)
|
||||
|
||||
const clearFlowchartCache = () => {
|
||||
for (let i = localStorage.length - 1; i >= 0; --i) {
|
||||
const key = localStorage.key(i)
|
||||
if (key && key.startsWith('flowchart_'))
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
}
|
||||
|
||||
const renderFlowchart = async (PrimitiveCode: string) => {
|
||||
try {
|
||||
const cachedSvg: any = localStorage.getItem(chartId.current)
|
||||
if (cachedSvg) {
|
||||
setSvgCode(cachedSvg)
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined' && mermaidAPI) {
|
||||
const svgGraph = await mermaidAPI.render(chartId.current, PrimitiveCode)
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
const base64Svg: any = await svgToBase64(svgGraph.svg)
|
||||
setSvgCode(base64Svg)
|
||||
if (chartId.current && base64Svg)
|
||||
localStorage.setItem(chartId.current, base64Svg)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
clearFlowchartCache()
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
handleReRender()
|
||||
}
|
||||
}
|
||||
|
||||
const svgToBase64 = (svgGraph: string) => {
|
||||
const svgBytes = new TextEncoder().encode(svgGraph)
|
||||
const blob = new Blob([svgBytes], { type: 'image/svg+xml;charset=utf-8' })
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => resolve(reader.result)
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
|
||||
const handleReRender = () => {
|
||||
setIsRender(false)
|
||||
setSvgCode(null)
|
||||
if (chartId.current)
|
||||
localStorage.removeItem(chartId.current)
|
||||
|
||||
setTimeout(() => {
|
||||
setIsRender(true)
|
||||
renderFlowchart(props.PrimitiveCode)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setIsRender(false)
|
||||
setTimeout(() => {
|
||||
setIsRender(true)
|
||||
renderFlowchart(props.PrimitiveCode)
|
||||
}, 100)
|
||||
}, [props.PrimitiveCode])
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
<div ref={ref}>
|
||||
{
|
||||
isRender
|
||||
&& <div id={chartId.current} className="mermaid" style={style}>
|
||||
{svgCode && <img src={svgCode} style={{ width: '100%', height: 'auto' }} alt="Mermaid chart" />}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default Flowchart
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { MessageMore } from '../type'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
|
||||
export type IMoreInfoProps = {
|
||||
more: MessageMore
|
||||
isQuestion: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
const MoreInfo: FC<IMoreInfoProps> = ({ more, isQuestion, className }) => {
|
||||
const { t } = useTranslation()
|
||||
return (<div className={`mt-1 w-full text-xs text-gray-400 ${isQuestion ? 'mr-2 text-right ' : 'pl-2 text-left float-right'} ${className}`}>
|
||||
<span className='mr-2'>{`${t('appLog.detail.timeConsuming')} ${more.latency}${t('appLog.detail.second')}`}</span>
|
||||
<span className='mr-2'>{`${t('appLog.detail.tokenCost')} ${formatNumber(more.tokens)}`}</span>
|
||||
<span className='mr-2'>·</span>
|
||||
<span>{more.time}</span>
|
||||
</div>)
|
||||
}
|
||||
export default React.memo(MoreInfo)
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
'use client'
|
||||
import React from 'react'
|
||||
|
||||
const OperationBtn = ({ innerContent, onClick, className }: { innerContent: React.ReactNode; onClick?: () => void; className?: string }) => (
|
||||
<div
|
||||
className={`relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-gray-500 hover:text-gray-800 ${className ?? ''}`}
|
||||
style={{ boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -2px rgba(0, 0, 0, 0.05)' }}
|
||||
onClick={onClick && onClick}
|
||||
>
|
||||
{innerContent}
|
||||
</div>
|
||||
)
|
||||
|
||||
export default OperationBtn
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useRef } from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import s from '../style.module.css'
|
||||
import type { IChatItem } from '../type'
|
||||
import MoreInfo from '../more-info'
|
||||
import AppContext from '@/context/app-context'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
import ImageGallery from '@/app/components/base/image-gallery'
|
||||
|
||||
type IQuestionProps = Pick<IChatItem, 'id' | 'content' | 'more' | 'useCurrentUserAvatar'> & {
|
||||
isShowPromptLog?: boolean
|
||||
item: IChatItem
|
||||
isResponding?: boolean
|
||||
}
|
||||
|
||||
const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar, isShowPromptLog, item }) => {
|
||||
const { userProfile } = useContext(AppContext)
|
||||
const userName = userProfile?.name
|
||||
const ref = useRef(null)
|
||||
const imgSrcs = item.message_files?.map(item => item.url)
|
||||
|
||||
return (
|
||||
<div className={`flex items-start justify-end ${isShowPromptLog && 'first-of-type:pt-[14px]'}`} key={id} ref={ref}>
|
||||
<div className={s.questionWrapWrap}>
|
||||
|
||||
<div className={`${s.question} group relative text-sm text-gray-900`}>
|
||||
<div
|
||||
className={'mr-2 py-3 px-4 bg-blue-500 rounded-tl-2xl rounded-b-2xl'}
|
||||
>
|
||||
{imgSrcs && imgSrcs.length > 0 && (
|
||||
<ImageGallery srcs={imgSrcs} />
|
||||
)}
|
||||
<Markdown content={content} />
|
||||
</div>
|
||||
</div>
|
||||
{more && <MoreInfo more={more} isQuestion={true} />}
|
||||
</div>
|
||||
{useCurrentUserAvatar
|
||||
? (
|
||||
<div className='w-10 h-10 shrink-0 leading-10 text-center mr-2 rounded-full bg-primary-600 text-white'>
|
||||
{userName?.[0].toLocaleUpperCase()}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className={`${s.questionIcon} w-10 h-10 shrink-0 `}></div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Question)
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
.answerIcon {
|
||||
position: relative;
|
||||
background: url(./icons/robot.svg) 100%/100%;
|
||||
}
|
||||
|
||||
.typeingIcon {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #FFFFFF;
|
||||
box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
|
||||
.questionIcon {
|
||||
background: url(./icons/default-avatar.jpg);
|
||||
background-size: contain;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.answer::before,
|
||||
.question::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 8px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.answer::before {
|
||||
left: 0;
|
||||
background: url(./icons/answer.svg) no-repeat;
|
||||
}
|
||||
|
||||
.copyBtn,
|
||||
.playBtn,
|
||||
.annotationBtn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
pre:hover .copyBtn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.answerWrapWrap,
|
||||
.questionWrapWrap {
|
||||
width: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.questionWrapWrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.question {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.answer {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.answerWrap:hover .copyBtn,
|
||||
.answerWrap:hover .playBtn,
|
||||
.answerWrap:hover .annotationBtn {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.answerWrap:hover .hasAnnotationBtn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.answerWrap .itemOperation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.answerWrap:hover .itemOperation {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.question::before {
|
||||
right: 0;
|
||||
background: url(./icons/question.svg) no-repeat;
|
||||
}
|
||||
|
||||
.textArea {
|
||||
padding-top: 13px;
|
||||
padding-bottom: 13px;
|
||||
padding-right: 130px;
|
||||
border-radius: 12px;
|
||||
line-height: 20px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.textArea:hover {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
/* .textArea:focus {
|
||||
box-shadow: 0px 3px 15px -3px rgba(0, 0, 0, 0.1), 0px 4px 6px rgba(0, 0, 0, 0.05);
|
||||
} */
|
||||
|
||||
.count {
|
||||
/* display: none; */
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.sendBtn {
|
||||
background: url(./icons/send.svg) center center no-repeat;
|
||||
}
|
||||
|
||||
.sendBtnActive {
|
||||
background-image: url(./icons/send-active.svg);
|
||||
}
|
||||
|
||||
.sendBtn:hover {
|
||||
background-image: url(./icons/send-active.svg);
|
||||
background-color: #EBF5FF;
|
||||
}
|
||||
|
||||
.textArea:focus+div .count {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.textArea:focus+div .sendBtn {
|
||||
background-image: url(./icons/send-active.svg);
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import React from 'react'
|
||||
import s from './style.module.css'
|
||||
|
||||
type ISVGBtnProps = {
|
||||
isSVG: boolean
|
||||
setIsSVG: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
const SVGBtn = ({
|
||||
isSVG,
|
||||
setIsSVG,
|
||||
}: ISVGBtnProps) => {
|
||||
return (
|
||||
<div
|
||||
className={'box-border p-0.5 flex items-center justify-center rounded-md bg-white cursor-pointer'}
|
||||
onClick={() => { setIsSVG(prevIsSVG => !prevIsSVG) }}
|
||||
>
|
||||
<div className={`w-6 h-6 rounded-md hover:bg-gray-50 ${s.svgIcon} ${isSVG ? s.svgIconed : ''}`}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SVGBtn
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
.svgIcon {
|
||||
background-image: url(~@/app/components/develop/secret-key/assets/svg.svg);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.svgIconed {
|
||||
background-image: url(~@/app/components/develop/secret-key/assets/svged.svg);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import type { ThoughtItem, ToolInfoInThought } from '../type'
|
||||
import Tool from '@/app/components/app/chat/thought/tool'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
|
||||
import I18n from '@/context/i18n'
|
||||
import { getLanguage } from '@/i18n/language'
|
||||
|
||||
export type IThoughtProps = {
|
||||
thought: ThoughtItem
|
||||
allToolIcons: Record<string, string | Emoji>
|
||||
isFinished: boolean
|
||||
}
|
||||
|
||||
function getValue(value: string, isValueArray: boolean, index: number) {
|
||||
if (isValueArray) {
|
||||
try {
|
||||
return JSON.parse(value)[index]
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
const Thought: FC<IThoughtProps> = ({
|
||||
thought,
|
||||
allToolIcons,
|
||||
isFinished,
|
||||
}) => {
|
||||
const { locale } = useContext(I18n)
|
||||
const language = getLanguage(locale)
|
||||
|
||||
const [toolNames, isValueArray]: [string[], boolean] = (() => {
|
||||
try {
|
||||
if (Array.isArray(JSON.parse(thought.tool)))
|
||||
return [JSON.parse(thought.tool), true]
|
||||
}
|
||||
catch (e) {
|
||||
}
|
||||
return [[thought.tool], false]
|
||||
})()
|
||||
|
||||
const toolThoughtList = toolNames.map((toolName, index) => {
|
||||
return {
|
||||
name: toolName,
|
||||
label: thought.tool_labels?.toolName?.language ?? toolName,
|
||||
input: getValue(thought.tool_input, isValueArray, index),
|
||||
output: getValue(thought.observation, isValueArray, index),
|
||||
isFinished,
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='my-2 space-y-2'>
|
||||
{toolThoughtList.map((item: ToolInfoInThought, index) => (
|
||||
<Tool
|
||||
key={index}
|
||||
payload={item}
|
||||
allToolIcons={allToolIcons}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Thought)
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
isRequest: boolean
|
||||
toolName: string
|
||||
content: string
|
||||
}
|
||||
|
||||
const Panel: FC<Props> = ({
|
||||
isRequest,
|
||||
toolName,
|
||||
content,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className='rounded-md bg-gray-100 overflow-hidden border border-black/5'>
|
||||
<div className='flex items-center px-2 py-1 leading-[18px] bg-gray-50 uppercase text-xs font-medium text-gray-500'>
|
||||
{t(`tools.thought.${isRequest ? 'requestTitle' : 'responseTitle'}`)} {toolName}
|
||||
</div>
|
||||
<div className='p-2 border-t border-black/5 leading-4 text-xs text-gray-700'>{content}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Panel)
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
.wrap {
|
||||
background-color: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
.wrapHoverEffect:hover{
|
||||
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.06), 0px 1px 3px 0px rgba(16, 24, 40, 0.1);
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import cn from 'classnames'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
RiLoader2Line,
|
||||
} from '@remixicon/react'
|
||||
import type { ToolInfoInThought } from '../type'
|
||||
import Panel from './panel'
|
||||
import { CheckCircle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import { DataSet as DataSetIcon } from '@/app/components/base/icons/src/public/thought'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
|
||||
type Props = {
|
||||
payload: ToolInfoInThought
|
||||
allToolIcons?: Record<string, string | Emoji>
|
||||
}
|
||||
|
||||
const getIcon = (toolName: string, allToolIcons: Record<string, string | Emoji>) => {
|
||||
if (toolName.startsWith('dataset_'))
|
||||
return <DataSetIcon className='shrink-0'></DataSetIcon>
|
||||
const icon = allToolIcons[toolName]
|
||||
if (!icon)
|
||||
return null
|
||||
return (
|
||||
typeof icon === 'string'
|
||||
? (
|
||||
<div
|
||||
className='w-3 h-3 bg-cover bg-center rounded-[3px] shrink-0'
|
||||
style={{
|
||||
backgroundImage: `url(${icon})`,
|
||||
}}
|
||||
></div>
|
||||
)
|
||||
: (
|
||||
<AppIcon
|
||||
className='rounded-[3px] shrink-0'
|
||||
size='xs'
|
||||
icon={icon?.content}
|
||||
background={icon?.background}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
const Tool: FC<Props> = ({
|
||||
payload,
|
||||
allToolIcons = {},
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { name, label, input, isFinished, output } = payload
|
||||
const toolName = name.startsWith('dataset_') ? t('dataset.knowledge') : name
|
||||
const toolLabel = name.startsWith('dataset_') ? t('dataset.knowledge') : label
|
||||
const [isShowDetail, setIsShowDetail] = useState(false)
|
||||
const icon = getIcon(name, allToolIcons) as any
|
||||
return (
|
||||
<div>
|
||||
<div className={cn(!isShowDetail && 'shadow-sm', !isShowDetail && 'inline-block', 'max-w-full overflow-x-auto bg-white rounded-md')}>
|
||||
<div
|
||||
className={cn('flex items-center h-7 px-2 cursor-pointer')}
|
||||
onClick={() => setIsShowDetail(!isShowDetail)}
|
||||
>
|
||||
{!isFinished && (
|
||||
<RiLoader2Line className='w-3 h-3 text-gray-500 animate-spin shrink-0' />
|
||||
)}
|
||||
{isFinished && !isShowDetail && (
|
||||
<CheckCircle className='w-3 h-3 text-[#12B76A] shrink-0' />
|
||||
)}
|
||||
{isFinished && isShowDetail && (
|
||||
icon
|
||||
)}
|
||||
<span className='mx-1 text-xs font-medium text-gray-500 shrink-0'>
|
||||
{t(`tools.thought.${isFinished ? 'used' : 'using'}`)}
|
||||
</span>
|
||||
<span
|
||||
className='text-xs font-medium text-gray-700 truncate'
|
||||
title={toolLabel}
|
||||
>
|
||||
{toolLabel}
|
||||
</span>
|
||||
<RiArrowDownSLine
|
||||
className={cn(isShowDetail && 'rotate-180', 'ml-1 w-3 h-3 text-gray-500 select-none cursor-pointer shrink-0')}
|
||||
/>
|
||||
</div>
|
||||
{isShowDetail && (
|
||||
<div className='border-t border-black/5 p-2 space-y-2 '>
|
||||
<Panel
|
||||
isRequest={true}
|
||||
toolName={toolName}
|
||||
content={input} />
|
||||
{output && (
|
||||
<Panel
|
||||
isRequest={false}
|
||||
toolName={toolName}
|
||||
content={output as string} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(Tool)
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
import type { TypeWithI18N } from '../../header/account-setting/model-provider-page/declarations'
|
||||
import type { Annotation, MessageRating } from '@/models/log'
|
||||
import type { VisionFile } from '@/types/app'
|
||||
|
||||
export type MessageMore = {
|
||||
time: string
|
||||
tokens: number
|
||||
latency: number | string
|
||||
}
|
||||
|
||||
export type Feedbacktype = {
|
||||
rating: MessageRating
|
||||
content?: string | null
|
||||
}
|
||||
|
||||
export type FeedbackFunc = (messageId: string, feedback: Feedbacktype) => Promise<any>
|
||||
export type SubmitAnnotationFunc = (messageId: string, content: string) => Promise<any>
|
||||
|
||||
export type DisplayScene = 'web' | 'console'
|
||||
|
||||
export type ToolInfoInThought = {
|
||||
name: string
|
||||
label: string
|
||||
input: string
|
||||
output: string
|
||||
isFinished: boolean
|
||||
}
|
||||
|
||||
export type ThoughtItem = {
|
||||
id: string
|
||||
tool: string // plugin or dataset. May has multi.
|
||||
thought: string
|
||||
tool_input: string
|
||||
tool_labels?: { [key: string]: TypeWithI18N }
|
||||
message_id: string
|
||||
observation: string
|
||||
position: number
|
||||
files?: string[]
|
||||
message_files?: VisionFile[]
|
||||
}
|
||||
|
||||
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
|
||||
citation?: CitationItem[]
|
||||
/**
|
||||
* Specific message type
|
||||
*/
|
||||
isAnswer: boolean
|
||||
/**
|
||||
* The user feedback result of this message
|
||||
*/
|
||||
feedback?: Feedbacktype
|
||||
/**
|
||||
* The admin feedback result of this message
|
||||
*/
|
||||
adminFeedback?: Feedbacktype
|
||||
/**
|
||||
* Whether to hide the feedback area
|
||||
*/
|
||||
feedbackDisabled?: boolean
|
||||
/**
|
||||
* More information about this message
|
||||
*/
|
||||
more?: MessageMore
|
||||
annotation?: Annotation
|
||||
useCurrentUserAvatar?: boolean
|
||||
isOpeningStatement?: boolean
|
||||
suggestedQuestions?: string[]
|
||||
log?: { role: string; text: string; files?: VisionFile[] }[]
|
||||
agent_thoughts?: ThoughtItem[]
|
||||
message_files?: VisionFile[]
|
||||
workflow_run_id?: string
|
||||
// for agent log
|
||||
conversationId?: string
|
||||
input?: any
|
||||
}
|
||||
|
||||
export type MessageEnd = {
|
||||
id: string
|
||||
metadata: {
|
||||
retriever_resources?: CitationItem[]
|
||||
annotation_reply: {
|
||||
id: string
|
||||
account: {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type MessageReplace = {
|
||||
id: string
|
||||
task_id: string
|
||||
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
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ import type { OnSend } from '@/app/components/base/chat/types'
|
|||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import {
|
||||
fetchConvesationMessages,
|
||||
fetchConversationMessages,
|
||||
fetchSuggestedQuestions,
|
||||
stopChatMessageResponding,
|
||||
} from '@/service/debug'
|
||||
|
|
@ -89,7 +89,7 @@ const ChatItem: FC<ChatItemProps> = ({
|
|||
`apps/${appId}/chat-messages`,
|
||||
data,
|
||||
{
|
||||
onGetConvesationMessages: (conversationId, getAbortController) => fetchConvesationMessages(appId, conversationId, getAbortController),
|
||||
onGetConvesationMessages: (conversationId, getAbortController) => fetchConversationMessages(appId, conversationId, getAbortController),
|
||||
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { useDebugConfigurationContext } from '@/context/debug-configuration'
|
|||
import type { OnSend } from '@/app/components/base/chat/types'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import {
|
||||
fetchConvesationMessages,
|
||||
fetchConversationMessages,
|
||||
fetchSuggestedQuestions,
|
||||
stopChatMessageResponding,
|
||||
} from '@/service/debug'
|
||||
|
|
@ -94,7 +94,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
|
|||
`apps/${appId}/chat-messages`,
|
||||
data,
|
||||
{
|
||||
onGetConvesationMessages: (conversationId, getAbortController) => fetchConvesationMessages(appId, conversationId, getAbortController),
|
||||
onGetConvesationMessages: (conversationId, getAbortController) => fetchConversationMessages(appId, conversationId, getAbortController),
|
||||
onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import {
|
||||
HandThumbDownIcon,
|
||||
|
|
@ -8,6 +8,7 @@ import {
|
|||
InformationCircleIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/react/24/outline'
|
||||
import { RiEditFill } from '@remixicon/react'
|
||||
import { get } from 'lodash-es'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
import dayjs from 'dayjs'
|
||||
|
|
@ -20,14 +21,13 @@ import cn from 'classnames'
|
|||
import s from './style.module.css'
|
||||
import VarPanel from './var-panel'
|
||||
import { randomString } from '@/utils'
|
||||
import { EditIconSolid } from '@/app/components/app/chat/icon-component'
|
||||
import type { FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc } from '@/app/components/app/chat/type'
|
||||
import type { ChatConversationFullDetailResponse, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationFullDetailResponse, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log'
|
||||
import type { FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc } from '@/app/components/base/chat/chat/type'
|
||||
import type { Annotation, ChatConversationFullDetailResponse, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationFullDetailResponse, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log'
|
||||
import type { App } from '@/types/app'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Drawer from '@/app/components/base/drawer'
|
||||
import Popover from '@/app/components/base/popover'
|
||||
import Chat from '@/app/components/app/chat'
|
||||
import Chat from '@/app/components/base/chat/chat'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
import { fetchChatConversationDetail, fetchChatMessages, fetchCompletionConversationDetail, updateLogMessageAnnotations, updateLogMessageFeedbacks } from '@/service/log'
|
||||
|
|
@ -38,8 +38,6 @@ import ModelName from '@/app/components/header/account-setting/model-provider-pa
|
|||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import TextGeneration from '@/app/components/app/text-generate/item'
|
||||
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
|
||||
import AgentLogModal from '@/app/components/base/agent-log-modal'
|
||||
import PromptLogModal from '@/app/components/base/prompt-log-modal'
|
||||
import MessageLogModal from '@/app/components/base/message-log-modal'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
|
@ -166,13 +164,9 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
|||
const { userProfile: { timezone } } = useAppContext()
|
||||
const { formatTime } = useTimestamp()
|
||||
const { onClose, appDetail } = useContext(DrawerContext)
|
||||
const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
|
||||
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
|
||||
currentLogItem: state.currentLogItem,
|
||||
setCurrentLogItem: state.setCurrentLogItem,
|
||||
showPromptLogModal: state.showPromptLogModal,
|
||||
setShowPromptLogModal: state.setShowPromptLogModal,
|
||||
showAgentLogModal: state.showAgentLogModal,
|
||||
setShowAgentLogModal: state.setShowAgentLogModal,
|
||||
showMessageLogModal: state.showMessageLogModal,
|
||||
setShowMessageLogModal: state.setShowMessageLogModal,
|
||||
currentLogModalActiveTab: state.currentLogModalActiveTab,
|
||||
|
|
@ -218,6 +212,72 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
|||
}
|
||||
}
|
||||
|
||||
const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
|
||||
setItems(items.map((item, i) => {
|
||||
if (i === index - 1) {
|
||||
return {
|
||||
...item,
|
||||
content: query,
|
||||
}
|
||||
}
|
||||
if (i === index) {
|
||||
return {
|
||||
...item,
|
||||
annotation: {
|
||||
...item.annotation,
|
||||
logAnnotation: {
|
||||
...item.annotation?.logAnnotation,
|
||||
content: answer,
|
||||
},
|
||||
} as any,
|
||||
}
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}, [items])
|
||||
const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => {
|
||||
setItems(items.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
|
||||
}))
|
||||
}, [items])
|
||||
const handleAnnotationRemoved = useCallback((index: number) => {
|
||||
setItems(items.map((item, i) => {
|
||||
if (i === index) {
|
||||
return {
|
||||
...item,
|
||||
content: item.content,
|
||||
annotation: undefined,
|
||||
}
|
||||
}
|
||||
return item
|
||||
}))
|
||||
}, [items])
|
||||
|
||||
useEffect(() => {
|
||||
if (appDetail?.id && detail.id && appDetail?.mode !== 'completion')
|
||||
fetchData()
|
||||
|
|
@ -374,24 +434,36 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
|||
isShowTextToSpeech
|
||||
appId={appDetail?.id}
|
||||
varList={varList}
|
||||
siteInfo={null}
|
||||
/>
|
||||
</div>
|
||||
: items.length < 8
|
||||
? <div className="px-2.5 pt-4 mb-4">
|
||||
? <div className="pt-4 mb-4">
|
||||
<Chat
|
||||
config={{
|
||||
appId: appDetail?.id,
|
||||
text_to_speech: {
|
||||
enabled: true,
|
||||
},
|
||||
supportAnnotation: true,
|
||||
annotation_reply: {
|
||||
enabled: true,
|
||||
},
|
||||
supportFeedback: true,
|
||||
} as any}
|
||||
chatList={items}
|
||||
isHideSendInput={true}
|
||||
onAnnotationAdded={handleAnnotationAdded}
|
||||
onAnnotationEdited={handleAnnotationEdited}
|
||||
onAnnotationRemoved={handleAnnotationRemoved}
|
||||
onFeedback={onFeedback}
|
||||
displayScene='console'
|
||||
isShowPromptLog
|
||||
supportAnnotation
|
||||
isShowTextToSpeech
|
||||
appId={appDetail?.id}
|
||||
onChatListChange={setItems}
|
||||
noChatInput
|
||||
showPromptLog
|
||||
hideProcessDetail
|
||||
chatContainerInnerClassName='px-6'
|
||||
/>
|
||||
</div>
|
||||
: <div
|
||||
className="px-2.5 py-4"
|
||||
className="py-4"
|
||||
id="scrollableDiv"
|
||||
style={{
|
||||
height: 1000, // Specify a value
|
||||
|
|
@ -422,35 +494,30 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
|||
inverse={true}
|
||||
>
|
||||
<Chat
|
||||
config={{
|
||||
app_id: appDetail?.id,
|
||||
text_to_speech: {
|
||||
enabled: true,
|
||||
},
|
||||
supportAnnotation: true,
|
||||
annotation_reply: {
|
||||
enabled: true,
|
||||
},
|
||||
supportFeedback: true,
|
||||
} as any}
|
||||
chatList={items}
|
||||
isHideSendInput={true}
|
||||
onAnnotationAdded={handleAnnotationAdded}
|
||||
onAnnotationEdited={handleAnnotationEdited}
|
||||
onAnnotationRemoved={handleAnnotationRemoved}
|
||||
onFeedback={onFeedback}
|
||||
displayScene='console'
|
||||
isShowPromptLog
|
||||
noChatInput
|
||||
showPromptLog
|
||||
hideProcessDetail
|
||||
chatContainerInnerClassName='px-6'
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
}
|
||||
{showPromptLogModal && (
|
||||
<PromptLogModal
|
||||
width={width}
|
||||
currentLogItem={currentLogItem}
|
||||
onCancel={() => {
|
||||
setCurrentLogItem()
|
||||
setShowPromptLogModal(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showAgentLogModal && (
|
||||
<AgentLogModal
|
||||
width={width}
|
||||
currentLogItem={currentLogItem}
|
||||
onCancel={() => {
|
||||
setCurrentLogItem()
|
||||
setShowAgentLogModal(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{showMessageLogModal && (
|
||||
<MessageLogModal
|
||||
width={width}
|
||||
|
|
@ -575,7 +642,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 })} ${formatTime(annotation?.created_at || dayjs().unix(), 'MM-DD hh:mm A')}`}
|
||||
<RiEditFill className='w-3 h-3 mr-1' />{`${t('appLog.detail.annotationTip', { user: annotation?.account?.name })} ${formatTime(annotation?.created_at || dayjs().unix(), 'MM-DD hh:mm A')}`}
|
||||
</span>
|
||||
}
|
||||
className={(isHighlight && !isChatMode) ? '' : '!hidden'}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import cn from 'classnames'
|
|||
import copy from 'copy-to-clipboard'
|
||||
import style from './style.module.css'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import copyStyle from '@/app/components/app/chat/copy-btn/style.module.css'
|
||||
import copyStyle from '@/app/components/base/copy-btn/style.module.css'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { create } from 'zustand'
|
||||
import type { App } from '@/types/app'
|
||||
import type { IChatItem } from '@/app/components/app/chat/type'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
|
||||
type State = {
|
||||
appDetail?: App
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { Markdown } from '@/app/components/base/markdown'
|
|||
import Loading from '@/app/components/base/loading'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import AudioBtn from '@/app/components/base/audio-btn'
|
||||
import type { Feedbacktype } from '@/app/components/app/chat/type'
|
||||
import type { Feedbacktype } from '@/app/components/base/chat/chat/type'
|
||||
import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
|
||||
import { File02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import { Bookmark } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
|
|
|||