| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030 |
- 'use client'
- import type { FC } from 'react'
- import React, { useCallback, useEffect, useRef, useState } from 'react'
- import useSWR from 'swr'
- import {
- HandThumbDownIcon,
- HandThumbUpIcon,
- } from '@heroicons/react/24/outline'
- import { RiCloseLine, RiEditFill } from '@remixicon/react'
- import { get } from 'lodash-es'
- import dayjs from 'dayjs'
- import utc from 'dayjs/plugin/utc'
- import timezone from 'dayjs/plugin/timezone'
- import { createContext, useContext } from 'use-context-selector'
- import { useShallow } from 'zustand/react/shallow'
- import { useTranslation } from 'react-i18next'
- import type { ChatItemInTree } from '../../base/chat/types'
- import Indicator from '../../header/indicator'
- import VarPanel from './var-panel'
- import type { FeedbackFunc, FeedbackType, IChatItem, SubmitAnnotationFunc } from '@/app/components/base/chat/chat/type'
- import type { Annotation, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log'
- import type { App } from '@/types/app'
- import ActionButton from '@/app/components/base/action-button'
- import Loading from '@/app/components/base/loading'
- import Drawer from '@/app/components/base/drawer'
- import Chat from '@/app/components/base/chat/chat'
- import { ToastContext } from '@/app/components/base/toast'
- import { fetchChatConversationDetail, fetchChatMessages, fetchCompletionConversationDetail, updateLogMessageAnnotations, updateLogMessageFeedbacks } from '@/service/log'
- import ModelInfo from '@/app/components/app/log/model-info'
- 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 MessageLogModal from '@/app/components/base/message-log-modal'
- import { useStore as useAppStore } from '@/app/components/app/store'
- import { useAppContext } from '@/context/app-context'
- import useTimestamp from '@/hooks/use-timestamp'
- import Tooltip from '@/app/components/base/tooltip'
- import { CopyIcon } from '@/app/components/base/copy-icon'
- import { buildChatItemTree, getThreadMessages } from '@/app/components/base/chat/utils'
- import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
- import cn from '@/utils/classnames'
- import { noop } from 'lodash-es'
- import PromptLogModal from '../../base/prompt-log-modal'
-
- dayjs.extend(utc)
- dayjs.extend(timezone)
-
- type IConversationList = {
- logs?: ChatConversationsResponse | CompletionConversationsResponse
- appDetail: App
- onRefresh: () => void
- }
-
- const defaultValue = 'N/A'
-
- type IDrawerContext = {
- onClose: () => void
- appDetail?: App
- }
-
- type StatusCount = {
- success: number
- failed: number
- partial_success: number
- }
-
- const DrawerContext = createContext<IDrawerContext>({} as IDrawerContext)
-
- /**
- * Icon component with numbers
- */
- const HandThumbIconWithCount: FC<{ count: number; iconType: 'up' | 'down' }> = ({ count, iconType }) => {
- const classname = iconType === 'up' ? 'text-primary-600 bg-primary-50' : 'text-red-600 bg-red-50'
- const Icon = iconType === 'up' ? HandThumbUpIcon : HandThumbDownIcon
- return <div className={`inline-flex w-fit items-center rounded-md p-1 text-xs ${classname} mr-1 last:mr-0`}>
- <Icon className={'mr-0.5 h-3 w-3 rounded-md'} />
- {count > 0 ? count : null}
- </div>
- }
-
- const statusTdRender = (statusCount: StatusCount) => {
- if (!statusCount)
- return null
-
- if (statusCount.partial_success + statusCount.failed === 0) {
- return (
- <div className='system-xs-semibold-uppercase inline-flex items-center gap-1'>
- <Indicator color={'green'} />
- <span className='text-util-colors-green-green-600'>Success</span>
- </div>
- )
- }
- else if (statusCount.failed === 0) {
- return (
- <div className='system-xs-semibold-uppercase inline-flex items-center gap-1'>
- <Indicator color={'green'} />
- <span className='text-util-colors-green-green-600'>Partial Success</span>
- </div>
- )
- }
- else {
- return (
- <div className='system-xs-semibold-uppercase inline-flex items-center gap-1'>
- <Indicator color={'red'} />
- <span className='text-util-colors-red-red-600'>{statusCount.failed} {`${statusCount.failed > 1 ? 'Failures' : 'Failure'}`}</span>
- </div>
- )
- }
- }
-
- const getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
- const newChatList: IChatItem[] = []
- try {
- messages.forEach((item: ChatMessage) => {
- const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
- newChatList.push({
- id: `question-${item.id}`,
- content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
- isAnswer: false,
- message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
- parentMessageId: item.parent_message_id || undefined,
- })
-
- const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
- newChatList.push({
- id: item.id,
- content: item.answer,
- agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
- feedback: item.feedbacks.find(item => item.from_source === 'user'), // user feedback
- adminFeedback: item.feedbacks.find(item => item.from_source === 'admin'), // admin feedback
- feedbackDisabled: false,
- isAnswer: true,
- message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
- log: [
- ...item.message,
- ...(item.message[item.message.length - 1]?.role !== 'assistant'
- ? [
- {
- role: 'assistant',
- text: item.answer,
- files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
- },
- ]
- : []),
- ] as IChatItem['log'],
- workflow_run_id: item.workflow_run_id,
- conversationId,
- input: {
- inputs: item.inputs,
- query: item.query,
- },
- more: {
- time: dayjs.unix(item.created_at).tz(timezone).format(format),
- tokens: item.answer_tokens + item.message_tokens,
- latency: item.provider_response_latency.toFixed(2),
- },
- citation: item.metadata?.retriever_resources,
- annotation: (() => {
- if (item.annotation_hit_history) {
- return {
- id: item.annotation_hit_history.annotation_id,
- authorName: item.annotation_hit_history.annotation_create_account?.name || 'N/A',
- created_at: item.annotation_hit_history.created_at,
- }
- }
-
- if (item.annotation) {
- return {
- id: item.annotation.id,
- authorName: item.annotation.account.name,
- logAnnotation: item.annotation,
- created_at: 0,
- }
- }
-
- return undefined
- })(),
- parentMessageId: `question-${item.id}`,
- })
- })
-
- return newChatList
- }
- catch (error) {
- console.error('getFormattedChatList processing failed:', error)
- throw error
- }
- }
-
- type IDetailPanel = {
- detail: any
- onFeedback: FeedbackFunc
- onSubmitAnnotation: SubmitAnnotationFunc
- }
-
- function DetailPanel({ detail, onFeedback }: IDetailPanel) {
- const MIN_ITEMS_FOR_SCROLL_LOADING = 8
- const SCROLL_THRESHOLD_PX = 50
- const SCROLL_DEBOUNCE_MS = 200
- const { userProfile: { timezone } } = useAppContext()
- const { formatTime } = useTimestamp()
- const { onClose, appDetail } = useContext(DrawerContext)
- const { notify } = useContext(ToastContext)
- const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, showPromptLogModal, setShowPromptLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
- currentLogItem: state.currentLogItem,
- setCurrentLogItem: state.setCurrentLogItem,
- showMessageLogModal: state.showMessageLogModal,
- setShowMessageLogModal: state.setShowMessageLogModal,
- showPromptLogModal: state.showPromptLogModal,
- setShowPromptLogModal: state.setShowPromptLogModal,
- currentLogModalActiveTab: state.currentLogModalActiveTab,
- })))
- const { t } = useTranslation()
- const [hasMore, setHasMore] = useState(true)
- const [varValues, setVarValues] = useState<Record<string, string>>({})
- const isLoadingRef = useRef(false)
-
- const [allChatItems, setAllChatItems] = useState<IChatItem[]>([])
- const [chatItemTree, setChatItemTree] = useState<ChatItemInTree[]>([])
- const [threadChatItems, setThreadChatItems] = useState<IChatItem[]>([])
-
- const fetchData = useCallback(async () => {
- if (isLoadingRef.current)
- return
-
- try {
- isLoadingRef.current = true
-
- if (!hasMore)
- return
-
- const params: ChatMessagesRequest = {
- conversation_id: detail.id,
- limit: 10,
- }
- // Use the oldest answer item ID for pagination
- const answerItems = allChatItems.filter(item => item.isAnswer)
- const oldestAnswerItem = answerItems[answerItems.length - 1]
- if (oldestAnswerItem?.id)
- params.first_id = oldestAnswerItem.id
- const messageRes = await fetchChatMessages({
- url: `/apps/${appDetail?.id}/chat-messages`,
- params,
- })
- if (messageRes.data.length > 0) {
- const varValues = messageRes.data.at(-1)!.inputs
- setVarValues(varValues)
- }
- setHasMore(messageRes.has_more)
-
- const newAllChatItems = [
- ...getFormattedChatList(messageRes.data, detail.id, timezone!, t('appLog.dateTimeFormat') as string),
- ...allChatItems,
- ]
- setAllChatItems(newAllChatItems)
-
- let tree = buildChatItemTree(newAllChatItems)
- if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
- tree = [{
- id: 'introduction',
- isAnswer: true,
- isOpeningStatement: true,
- content: detail?.model_config?.configs?.introduction ?? 'hello',
- feedbackDisabled: true,
- children: tree,
- }]
- }
- setChatItemTree(tree)
-
- const lastMessageId = newAllChatItems.length > 0 ? newAllChatItems[newAllChatItems.length - 1].id : undefined
- setThreadChatItems(getThreadMessages(tree, lastMessageId))
- }
- catch (err) {
- console.error('fetchData execution failed:', err)
- }
- finally {
- isLoadingRef.current = false
- }
- }, [allChatItems, detail.id, hasMore, timezone, t, appDetail, detail?.model_config?.configs?.introduction])
-
- const switchSibling = useCallback((siblingMessageId: string) => {
- const newThreadChatItems = getThreadMessages(chatItemTree, siblingMessageId)
- setThreadChatItems(newThreadChatItems)
- }, [chatItemTree])
-
- const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
- setAllChatItems(allChatItems.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
- }))
- }, [allChatItems])
- const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => {
- setAllChatItems(allChatItems.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
- }))
- }, [allChatItems])
- const handleAnnotationRemoved = useCallback(async (index: number): Promise<boolean> => {
- const annotation = allChatItems[index]?.annotation
-
- try {
- if (annotation?.id) {
- const { delAnnotation } = await import('@/service/annotation')
- await delAnnotation(appDetail?.id || '', annotation.id)
- }
-
- setAllChatItems(allChatItems.map((item, i) => {
- if (i === index) {
- return {
- ...item,
- content: item.content,
- annotation: undefined,
- }
- }
- return item
- }))
-
- notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
- return true
- }
- catch {
- notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
- return false
- }
- }, [allChatItems, appDetail?.id, t])
-
- const fetchInitiated = useRef(false)
-
- // Only load initial messages, don't auto-load more
- useEffect(() => {
- if (appDetail?.id && detail.id && appDetail?.mode !== 'completion' && !fetchInitiated.current) {
- // Mark as initialized, but don't auto-load more messages
- fetchInitiated.current = true
- // Still call fetchData to get initial messages
- fetchData()
- }
- }, [appDetail?.id, detail.id, appDetail?.mode, fetchData])
-
- const [isLoading, setIsLoading] = useState(false)
-
- const loadMoreMessages = useCallback(async () => {
- if (isLoading || !hasMore || !appDetail?.id || !detail.id)
- return
-
- setIsLoading(true)
-
- try {
- const params: ChatMessagesRequest = {
- conversation_id: detail.id,
- limit: 10,
- }
-
- // Use the earliest response item as the first_id
- const answerItems = allChatItems.filter(item => item.isAnswer)
- const oldestAnswerItem = answerItems[answerItems.length - 1]
- if (oldestAnswerItem?.id) {
- params.first_id = oldestAnswerItem.id
- }
- else if (allChatItems.length > 0 && allChatItems[0]?.id) {
- const firstId = allChatItems[0].id.replace('question-', '').replace('answer-', '')
- params.first_id = firstId
- }
-
- const messageRes = await fetchChatMessages({
- url: `/apps/${appDetail.id}/chat-messages`,
- params,
- })
-
- if (!messageRes.data || messageRes.data.length === 0) {
- setHasMore(false)
- return
- }
-
- if (messageRes.data.length > 0) {
- const varValues = messageRes.data.at(-1)!.inputs
- setVarValues(varValues)
- }
-
- setHasMore(messageRes.has_more)
-
- const newItems = getFormattedChatList(
- messageRes.data,
- detail.id,
- timezone!,
- t('appLog.dateTimeFormat') as string,
- )
-
- // Check for duplicate messages
- const existingIds = new Set(allChatItems.map(item => item.id))
- const uniqueNewItems = newItems.filter(item => !existingIds.has(item.id))
-
- if (uniqueNewItems.length === 0) {
- if (allChatItems.length > 1) {
- const nextId = allChatItems[1].id.replace('question-', '').replace('answer-', '')
-
- const retryParams = {
- ...params,
- first_id: nextId,
- }
-
- const retryRes = await fetchChatMessages({
- url: `/apps/${appDetail.id}/chat-messages`,
- params: retryParams,
- })
-
- if (retryRes.data && retryRes.data.length > 0) {
- const retryItems = getFormattedChatList(
- retryRes.data,
- detail.id,
- timezone!,
- t('appLog.dateTimeFormat') as string,
- )
-
- const retryUniqueItems = retryItems.filter(item => !existingIds.has(item.id))
- if (retryUniqueItems.length > 0) {
- const newAllChatItems = [
- ...retryUniqueItems,
- ...allChatItems,
- ]
-
- setAllChatItems(newAllChatItems)
-
- let tree = buildChatItemTree(newAllChatItems)
- if (retryRes.has_more === false && detail?.model_config?.configs?.introduction) {
- tree = [{
- id: 'introduction',
- isAnswer: true,
- isOpeningStatement: true,
- content: detail?.model_config?.configs?.introduction ?? 'hello',
- feedbackDisabled: true,
- children: tree,
- }]
- }
- setChatItemTree(tree)
- setHasMore(retryRes.has_more)
- setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id))
- return
- }
- }
- }
- }
-
- const newAllChatItems = [
- ...uniqueNewItems,
- ...allChatItems,
- ]
-
- setAllChatItems(newAllChatItems)
-
- let tree = buildChatItemTree(newAllChatItems)
- if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
- tree = [{
- id: 'introduction',
- isAnswer: true,
- isOpeningStatement: true,
- content: detail?.model_config?.configs?.introduction ?? 'hello',
- feedbackDisabled: true,
- children: tree,
- }]
- }
- setChatItemTree(tree)
-
- setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id))
- }
- catch (error) {
- console.error(error)
- setHasMore(false)
- }
- finally {
- setIsLoading(false)
- }
- }, [allChatItems, detail.id, hasMore, isLoading, timezone, t, appDetail])
-
- useEffect(() => {
- const scrollableDiv = document.getElementById('scrollableDiv')
- const outerDiv = scrollableDiv?.parentElement
- const chatContainer = document.querySelector('.mx-1.mb-1.grow.overflow-auto') as HTMLElement
-
- let scrollContainer: HTMLElement | null = null
-
- if (outerDiv && outerDiv.scrollHeight > outerDiv.clientHeight) {
- scrollContainer = outerDiv
- }
- else if (scrollableDiv && scrollableDiv.scrollHeight > scrollableDiv.clientHeight) {
- scrollContainer = scrollableDiv
- }
- else if (chatContainer && chatContainer.scrollHeight > chatContainer.clientHeight) {
- scrollContainer = chatContainer
- }
- else {
- const possibleContainers = document.querySelectorAll('.overflow-auto, .overflow-y-auto')
- for (let i = 0; i < possibleContainers.length; i++) {
- const container = possibleContainers[i] as HTMLElement
- if (container.scrollHeight > container.clientHeight) {
- scrollContainer = container
- break
- }
- }
- }
-
- if (!scrollContainer)
- return
-
- let lastLoadTime = 0
- const throttleDelay = 200
-
- const handleScroll = () => {
- const currentScrollTop = scrollContainer!.scrollTop
- const scrollHeight = scrollContainer!.scrollHeight
- const clientHeight = scrollContainer!.clientHeight
-
- const distanceFromTop = currentScrollTop
- const distanceFromBottom = scrollHeight - currentScrollTop - clientHeight
-
- const now = Date.now()
-
- const isNearTop = distanceFromTop < 30
- // eslint-disable-next-line sonarjs/no-unused-vars
- const _distanceFromBottom = distanceFromBottom < 30
- if (isNearTop && hasMore && !isLoading && (now - lastLoadTime > throttleDelay)) {
- lastLoadTime = now
- loadMoreMessages()
- }
- }
-
- scrollContainer.addEventListener('scroll', handleScroll, { passive: true })
-
- const handleWheel = (e: WheelEvent) => {
- if (e.deltaY < 0)
- handleScroll()
- }
- scrollContainer.addEventListener('wheel', handleWheel, { passive: true })
-
- return () => {
- scrollContainer!.removeEventListener('scroll', handleScroll)
- scrollContainer!.removeEventListener('wheel', handleWheel)
- }
- }, [hasMore, isLoading, loadMoreMessages])
-
- const isChatMode = appDetail?.mode !== 'completion'
- const isAdvanced = appDetail?.mode === 'advanced-chat'
-
- const varList = (detail.model_config as any).user_input_form?.map((item: any) => {
- const itemContent = item[Object.keys(item)[0]]
- return {
- label: itemContent.variable,
- value: varValues[itemContent.variable] || detail.message?.inputs?.[itemContent.variable],
- }
- }) || []
- const message_files = (!isChatMode && detail.message.message_files && detail.message.message_files.length > 0)
- ? detail.message.message_files.map((item: any) => item.url)
- : []
-
- const [width, setWidth] = useState(0)
- const ref = useRef<HTMLDivElement>(null)
-
- const adjustModalWidth = () => {
- if (ref.current)
- setWidth(document.body.clientWidth - (ref.current?.clientWidth + 16) - 8)
- }
-
- useEffect(() => {
- const raf = requestAnimationFrame(adjustModalWidth)
- return () => cancelAnimationFrame(raf)
- }, [])
-
- // Add scroll listener to ensure loading is triggered
- useEffect(() => {
- if (threadChatItems.length >= MIN_ITEMS_FOR_SCROLL_LOADING && hasMore) {
- const scrollableDiv = document.getElementById('scrollableDiv')
-
- if (scrollableDiv) {
- let loadingTimeout: NodeJS.Timeout | null = null
-
- const handleScroll = () => {
- const { scrollTop } = scrollableDiv
-
- // Trigger loading when scrolling near the top
- if (scrollTop < SCROLL_THRESHOLD_PX && !isLoadingRef.current) {
- if (loadingTimeout)
- clearTimeout(loadingTimeout)
-
- loadingTimeout = setTimeout(fetchData, SCROLL_DEBOUNCE_MS) // 200ms debounce
- }
- }
-
- scrollableDiv.addEventListener('scroll', handleScroll)
- return () => {
- scrollableDiv.removeEventListener('scroll', handleScroll)
- if (loadingTimeout)
- clearTimeout(loadingTimeout)
- }
- }
- }
- }, [threadChatItems.length, hasMore, fetchData])
-
- return (
- <div ref={ref} className='flex h-full flex-col rounded-xl border-[0.5px] border-components-panel-border'>
- {/* Panel Header */}
- <div className='flex shrink-0 items-center gap-2 rounded-t-xl bg-components-panel-bg pb-2 pl-4 pr-3 pt-3'>
- <div className='shrink-0'>
- <div className='system-xs-semibold-uppercase mb-0.5 text-text-primary'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</div>
- {isChatMode && (
- <div className='system-2xs-regular-uppercase flex items-center text-text-secondary'>
- <Tooltip
- popupContent={detail.id}
- >
- <div className='truncate'>{detail.id}</div>
- </Tooltip>
- <CopyIcon content={detail.id} />
- </div>
- )}
- {!isChatMode && (
- <div className='system-2xs-regular-uppercase text-text-secondary'>{formatTime(detail.created_at, t('appLog.dateTimeFormat') as string)}</div>
- )}
- </div>
- <div className='flex grow flex-wrap items-center justify-end gap-y-1'>
- {!isAdvanced && <ModelInfo model={detail.model_config.model} />}
- </div>
- <ActionButton size='l' onClick={onClose}>
- <RiCloseLine className='h-4 w-4 text-text-tertiary' />
- </ActionButton>
- </div>
- {/* Panel Body */}
- <div className='shrink-0 px-1 pt-1'>
- <div className='rounded-t-xl bg-background-section-burn p-3 pb-2'>
- {(varList.length > 0 || (!isChatMode && message_files.length > 0)) && (
- <VarPanel
- varList={varList}
- message_files={message_files}
- />
- )}
- </div>
- </div>
- <div className='mx-1 mb-1 grow overflow-auto rounded-b-xl bg-background-section-burn'>
- {!isChatMode
- ? <div className="px-6 py-4">
- <div className='flex h-[18px] items-center space-x-3'>
- <div className='system-xs-semibold-uppercase text-text-tertiary'>{t('appLog.table.header.output')}</div>
- <div className='h-[1px] grow' style={{
- background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, rgb(243, 244, 246) 100%)',
- }}></div>
- </div>
- <TextGeneration
- className='mt-2'
- content={detail.message.answer}
- messageId={detail.message.id}
- isError={false}
- onRetry={noop}
- isInstalledApp={false}
- supportFeedback
- feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
- onFeedback={feedback => onFeedback(detail.message.id, feedback)}
- isShowTextToSpeech
- siteInfo={null}
- />
- </div>
- : threadChatItems.length < MIN_ITEMS_FOR_SCROLL_LOADING ? (
- <div className="mb-4 pt-4">
- <Chat
- config={{
- appId: appDetail?.id,
- text_to_speech: {
- enabled: true,
- },
- questionEditEnable: false,
- supportAnnotation: true,
- annotation_reply: {
- enabled: true,
- },
- supportFeedback: true,
- } as any}
- chatList={threadChatItems}
- onAnnotationAdded={handleAnnotationAdded}
- onAnnotationEdited={handleAnnotationEdited}
- onAnnotationRemoved={handleAnnotationRemoved}
- onFeedback={onFeedback}
- noChatInput
- showPromptLog
- hideProcessDetail
- chatContainerInnerClassName='px-3'
- switchSibling={switchSibling}
- />
- </div>
- ) : (
- <div
- className="py-4"
- id="scrollableDiv"
- style={{
- display: 'flex',
- flexDirection: 'column-reverse',
- height: '100%',
- overflow: 'auto',
- }}>
- {/* Put the scroll bar always on the bottom */}
- <div className="flex w-full flex-col-reverse" style={{ position: 'relative' }}>
- {/* Loading state indicator - only shown when loading */}
- {hasMore && isLoading && (
- <div className="sticky left-0 right-0 top-0 z-10 bg-primary-50/40 py-3 text-center">
- <div className='system-xs-regular text-text-tertiary'>
- {t('appLog.detail.loading')}...
- </div>
- </div>
- )}
-
- <Chat
- config={{
- appId: appDetail?.id,
- text_to_speech: {
- enabled: true,
- },
- questionEditEnable: false,
- supportAnnotation: true,
- annotation_reply: {
- enabled: true,
- },
- supportFeedback: true,
- } as any}
- chatList={threadChatItems}
- onAnnotationAdded={handleAnnotationAdded}
- onAnnotationEdited={handleAnnotationEdited}
- onAnnotationRemoved={handleAnnotationRemoved}
- onFeedback={onFeedback}
- noChatInput
- showPromptLog
- hideProcessDetail
- chatContainerInnerClassName='px-3'
- switchSibling={switchSibling}
- />
- </div>
- </div>
- )
- }
- </div>
- {showMessageLogModal && (
- <MessageLogModal
- width={width}
- currentLogItem={currentLogItem}
- onCancel={() => {
- setCurrentLogItem()
- setShowMessageLogModal(false)
- }}
- defaultTab={currentLogModalActiveTab}
- />
- )}
- {!isChatMode && showPromptLogModal && (
- <PromptLogModal
- width={width}
- currentLogItem={currentLogItem}
- onCancel={() => {
- setCurrentLogItem()
- setShowPromptLogModal(false)
- }}
- />
- )}
- </div>
- )
- }
-
- /**
- * Text App Conversation Detail Component
- */
- const CompletionConversationDetailComp: FC<{ appId?: string; conversationId?: string }> = ({ appId, conversationId }) => {
- // Text Generator App Session Details Including Message List
- const detailParams = ({ url: `/apps/${appId}/completion-conversations/${conversationId}` })
- const { data: conversationDetail, mutate: conversationDetailMutate } = useSWR(() => (appId && conversationId) ? detailParams : null, fetchCompletionConversationDetail)
- const { notify } = useContext(ToastContext)
- const { t } = useTranslation()
-
- const handleFeedback = async (mid: string, { rating }: FeedbackType): Promise<boolean> => {
- try {
- await updateLogMessageFeedbacks({ url: `/apps/${appId}/feedbacks`, body: { message_id: mid, rating } })
- conversationDetailMutate()
- notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
- return true
- }
- catch {
- notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
- return false
- }
- }
-
- const handleAnnotation = async (mid: string, value: string): Promise<boolean> => {
- try {
- await updateLogMessageAnnotations({ url: `/apps/${appId}/annotations`, body: { message_id: mid, content: value } })
- conversationDetailMutate()
- notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
- return true
- }
- catch {
- notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
- return false
- }
- }
-
- if (!conversationDetail)
- return null
-
- return <DetailPanel
- detail={conversationDetail}
- onFeedback={handleFeedback}
- onSubmitAnnotation={handleAnnotation}
- />
- }
-
- /**
- * Chat App Conversation Detail Component
- */
- const ChatConversationDetailComp: FC<{ appId?: string; conversationId?: string }> = ({ appId, conversationId }) => {
- const detailParams = { url: `/apps/${appId}/chat-conversations/${conversationId}` }
- const { data: conversationDetail } = useSWR(() => (appId && conversationId) ? detailParams : null, fetchChatConversationDetail)
- const { notify } = useContext(ToastContext)
- const { t } = useTranslation()
-
- const handleFeedback = async (mid: string, { rating }: FeedbackType): Promise<boolean> => {
- try {
- await updateLogMessageFeedbacks({ url: `/apps/${appId}/feedbacks`, body: { message_id: mid, rating } })
- notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
- return true
- }
- catch {
- notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
- return false
- }
- }
-
- const handleAnnotation = async (mid: string, value: string): Promise<boolean> => {
- try {
- await updateLogMessageAnnotations({ url: `/apps/${appId}/annotations`, body: { message_id: mid, content: value } })
- notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
- return true
- }
- catch {
- notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
- return false
- }
- }
-
- if (!conversationDetail)
- return null
-
- return <DetailPanel
- detail={conversationDetail}
- onFeedback={handleFeedback}
- onSubmitAnnotation={handleAnnotation}
- />
- }
-
- /**
- * Conversation list component including basic information
- */
- const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh }) => {
- const { t } = useTranslation()
- const { formatTime } = useTimestamp()
-
- const media = useBreakpoints()
- const isMobile = media === MediaType.mobile
-
- const [showDrawer, setShowDrawer] = useState<boolean>(false) // Whether to display the chat details drawer
- const [currentConversation, setCurrentConversation] = useState<ChatConversationGeneralDetail | CompletionConversationGeneralDetail | undefined>() // Currently selected conversation
- const isChatMode = appDetail.mode !== 'completion' // Whether the app is a chat app
- const isChatflow = appDetail.mode === 'advanced-chat' // Whether the app is a chatflow app
- const { setShowPromptLogModal, setShowAgentLogModal, setShowMessageLogModal } = useAppStore(useShallow(state => ({
- setShowPromptLogModal: state.setShowPromptLogModal,
- setShowAgentLogModal: state.setShowAgentLogModal,
- setShowMessageLogModal: state.setShowMessageLogModal,
- })))
-
- // Annotated data needs to be highlighted
- const renderTdValue = (value: string | number | null, isEmptyStyle: boolean, isHighlight = false, annotation?: LogAnnotation) => {
- return (
- <Tooltip
- popupContent={
- <span className='inline-flex items-center text-xs text-text-tertiary'>
- <RiEditFill className='mr-1 h-3 w-3' />{`${t('appLog.detail.annotationTip', { user: annotation?.account?.name })} ${formatTime(annotation?.created_at || dayjs().unix(), 'MM-DD hh:mm A')}`}
- </span>
- }
- popupClassName={(isHighlight && !isChatMode) ? '' : '!hidden'}
- >
- <div className={cn(isEmptyStyle ? 'text-text-quaternary' : 'text-text-secondary', !isHighlight ? '' : 'bg-orange-100', 'system-sm-regular overflow-hidden text-ellipsis whitespace-nowrap')}>
- {value || '-'}
- </div>
- </Tooltip>
- )
- }
-
- const onCloseDrawer = () => {
- onRefresh()
- setShowDrawer(false)
- setCurrentConversation(undefined)
- setShowPromptLogModal(false)
- setShowAgentLogModal(false)
- setShowMessageLogModal(false)
- }
-
- if (!logs)
- return <Loading />
-
- return (
- <div className='relative grow overflow-x-auto'>
- <table className={cn('mt-2 w-full min-w-[440px] border-collapse border-0')}>
- <thead className='system-xs-medium-uppercase text-text-tertiary'>
- <tr>
- <td className='w-5 whitespace-nowrap rounded-l-lg bg-background-section-burn pl-2 pr-1'></td>
- <td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>{isChatMode ? t('appLog.table.header.summary') : t('appLog.table.header.input')}</td>
- <td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>{t('appLog.table.header.endUser')}</td>
- {isChatflow && <td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>{t('appLog.table.header.status')}</td>}
- <td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>{isChatMode ? t('appLog.table.header.messageCount') : t('appLog.table.header.output')}</td>
- <td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>{t('appLog.table.header.userRate')}</td>
- <td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>{t('appLog.table.header.adminRate')}</td>
- <td className='whitespace-nowrap bg-background-section-burn py-1.5 pl-3'>{t('appLog.table.header.updatedTime')}</td>
- <td className='whitespace-nowrap rounded-r-lg bg-background-section-burn py-1.5 pl-3'>{t('appLog.table.header.time')}</td>
- </tr>
- </thead>
- <tbody className="system-sm-regular text-text-secondary">
- {logs.data.map((log: any) => {
- const endUser = log.from_end_user_session_id || log.from_account_name
- const leftValue = get(log, isChatMode ? 'name' : 'message.inputs.query') || (!isChatMode ? (get(log, 'message.query') || get(log, 'message.inputs.default_input')) : '') || ''
- const rightValue = get(log, isChatMode ? 'message_count' : 'message.answer')
- return <tr
- key={log.id}
- className={cn('cursor-pointer border-b border-divider-subtle hover:bg-background-default-hover', currentConversation?.id !== log.id ? '' : 'bg-background-default-hover')}
- onClick={() => {
- setShowDrawer(true)
- setCurrentConversation(log)
- }}>
- <td className='h-4'>
- {!log.read_at && (
- <div className='flex items-center p-3 pr-0.5'>
- <span className='inline-block h-1.5 w-1.5 rounded bg-util-colors-blue-blue-500'></span>
- </div>
- )}
- </td>
- <td className='w-[160px] p-3 pr-2' style={{ maxWidth: isChatMode ? 300 : 200 }}>
- {renderTdValue(leftValue || t('appLog.table.empty.noChat'), !leftValue, isChatMode && log.annotated)}
- </td>
- <td className='p-3 pr-2'>{renderTdValue(endUser || defaultValue, !endUser)}</td>
- {isChatflow && <td className='w-[160px] p-3 pr-2' style={{ maxWidth: isChatMode ? 300 : 200 }}>
- {statusTdRender(log.status_count)}
- </td>}
- <td className='p-3 pr-2' style={{ maxWidth: isChatMode ? 100 : 200 }}>
- {renderTdValue(rightValue === 0 ? 0 : (rightValue || t('appLog.table.empty.noOutput')), !rightValue, !isChatMode && !!log.annotation?.content, log.annotation)}
- </td>
- <td className='p-3 pr-2'>
- {(!log.user_feedback_stats.like && !log.user_feedback_stats.dislike)
- ? renderTdValue(defaultValue, true)
- : <>
- {!!log.user_feedback_stats.like && <HandThumbIconWithCount iconType='up' count={log.user_feedback_stats.like} />}
- {!!log.user_feedback_stats.dislike && <HandThumbIconWithCount iconType='down' count={log.user_feedback_stats.dislike} />}
- </>
- }
- </td>
- <td className='p-3 pr-2'>
- {(!log.admin_feedback_stats.like && !log.admin_feedback_stats.dislike)
- ? renderTdValue(defaultValue, true)
- : <>
- {!!log.admin_feedback_stats.like && <HandThumbIconWithCount iconType='up' count={log.admin_feedback_stats.like} />}
- {!!log.admin_feedback_stats.dislike && <HandThumbIconWithCount iconType='down' count={log.admin_feedback_stats.dislike} />}
- </>
- }
- </td>
- <td className='w-[160px] p-3 pr-2'>{formatTime(log.updated_at, t('appLog.dateTimeFormat') as string)}</td>
- <td className='w-[160px] p-3 pr-2'>{formatTime(log.created_at, t('appLog.dateTimeFormat') as string)}</td>
- </tr>
- })}
- </tbody>
- </table>
- <Drawer
- isOpen={showDrawer}
- onClose={onCloseDrawer}
- mask={isMobile}
- footer={null}
- panelClassName='mt-16 mx-2 sm:mr-2 mb-4 !p-0 !max-w-[640px] rounded-xl bg-components-panel-bg'
- >
- <DrawerContext.Provider value={{
- onClose: onCloseDrawer,
- appDetail,
- }}>
- {isChatMode
- ? <ChatConversationDetailComp appId={appDetail.id} conversationId={currentConversation?.id} />
- : <CompletionConversationDetailComp appId={appDetail.id} conversationId={currentConversation?.id} />
- }
- </DrawerContext.Provider>
- </Drawer>
- </div>
- )
- }
-
- export default ConversationList
|