list.tsx 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { useCallback, useEffect, useRef, useState } from 'react'
  4. import useSWR from 'swr'
  5. import {
  6. HandThumbDownIcon,
  7. HandThumbUpIcon,
  8. } from '@heroicons/react/24/outline'
  9. import { RiCloseLine, RiEditFill } from '@remixicon/react'
  10. import { get } from 'lodash-es'
  11. import InfiniteScroll from 'react-infinite-scroll-component'
  12. import dayjs from 'dayjs'
  13. import utc from 'dayjs/plugin/utc'
  14. import timezone from 'dayjs/plugin/timezone'
  15. import { createContext, useContext } from 'use-context-selector'
  16. import { useShallow } from 'zustand/react/shallow'
  17. import { useTranslation } from 'react-i18next'
  18. import type { ChatItemInTree } from '../../base/chat/types'
  19. import VarPanel from './var-panel'
  20. import type { FeedbackFunc, FeedbackType, IChatItem, SubmitAnnotationFunc } from '@/app/components/base/chat/chat/type'
  21. import type { Annotation, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log'
  22. import type { App } from '@/types/app'
  23. import ActionButton from '@/app/components/base/action-button'
  24. import Loading from '@/app/components/base/loading'
  25. import Drawer from '@/app/components/base/drawer'
  26. import Chat from '@/app/components/base/chat/chat'
  27. import { ToastContext } from '@/app/components/base/toast'
  28. import { fetchChatConversationDetail, fetchChatMessages, fetchCompletionConversationDetail, updateLogMessageAnnotations, updateLogMessageFeedbacks } from '@/service/log'
  29. import ModelInfo from '@/app/components/app/log/model-info'
  30. import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
  31. import TextGeneration from '@/app/components/app/text-generate/item'
  32. import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
  33. import MessageLogModal from '@/app/components/base/message-log-modal'
  34. import PromptLogModal from '@/app/components/base/prompt-log-modal'
  35. import { useStore as useAppStore } from '@/app/components/app/store'
  36. import { useAppContext } from '@/context/app-context'
  37. import useTimestamp from '@/hooks/use-timestamp'
  38. import Tooltip from '@/app/components/base/tooltip'
  39. import { CopyIcon } from '@/app/components/base/copy-icon'
  40. import { buildChatItemTree, getThreadMessages } from '@/app/components/base/chat/utils'
  41. import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
  42. import cn from '@/utils/classnames'
  43. dayjs.extend(utc)
  44. dayjs.extend(timezone)
  45. type IConversationList = {
  46. logs?: ChatConversationsResponse | CompletionConversationsResponse
  47. appDetail: App
  48. onRefresh: () => void
  49. }
  50. const defaultValue = 'N/A'
  51. type IDrawerContext = {
  52. onClose: () => void
  53. appDetail?: App
  54. }
  55. const DrawerContext = createContext<IDrawerContext>({} as IDrawerContext)
  56. /**
  57. * Icon component with numbers
  58. */
  59. const HandThumbIconWithCount: FC<{ count: number; iconType: 'up' | 'down' }> = ({ count, iconType }) => {
  60. const classname = iconType === 'up' ? 'text-primary-600 bg-primary-50' : 'text-red-600 bg-red-50'
  61. const Icon = iconType === 'up' ? HandThumbUpIcon : HandThumbDownIcon
  62. return <div className={`inline-flex items-center w-fit rounded-md p-1 text-xs ${classname} mr-1 last:mr-0`}>
  63. <Icon className={'h-3 w-3 mr-0.5 rounded-md'} />
  64. {count > 0 ? count : null}
  65. </div>
  66. }
  67. const getFormattedChatList = (messages: ChatMessage[], conversationId: string, timezone: string, format: string) => {
  68. const newChatList: IChatItem[] = []
  69. messages.forEach((item: ChatMessage) => {
  70. const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
  71. newChatList.push({
  72. id: `question-${item.id}`,
  73. content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
  74. isAnswer: false,
  75. message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
  76. parentMessageId: item.parent_message_id || undefined,
  77. })
  78. const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
  79. newChatList.push({
  80. id: item.id,
  81. content: item.answer,
  82. agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
  83. feedback: item.feedbacks.find(item => item.from_source === 'user'), // user feedback
  84. adminFeedback: item.feedbacks.find(item => item.from_source === 'admin'), // admin feedback
  85. feedbackDisabled: false,
  86. isAnswer: true,
  87. message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
  88. log: [
  89. ...item.message,
  90. ...(item.message[item.message.length - 1]?.role !== 'assistant'
  91. ? [
  92. {
  93. role: 'assistant',
  94. text: item.answer,
  95. files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
  96. },
  97. ]
  98. : []),
  99. ] as IChatItem['log'],
  100. workflow_run_id: item.workflow_run_id,
  101. conversationId,
  102. input: {
  103. inputs: item.inputs,
  104. query: item.query,
  105. },
  106. more: {
  107. time: dayjs.unix(item.created_at).tz(timezone).format(format),
  108. tokens: item.answer_tokens + item.message_tokens,
  109. latency: item.provider_response_latency.toFixed(2),
  110. },
  111. citation: item.metadata?.retriever_resources,
  112. annotation: (() => {
  113. if (item.annotation_hit_history) {
  114. return {
  115. id: item.annotation_hit_history.annotation_id,
  116. authorName: item.annotation_hit_history.annotation_create_account?.name || 'N/A',
  117. created_at: item.annotation_hit_history.created_at,
  118. }
  119. }
  120. if (item.annotation) {
  121. return {
  122. id: item.annotation.id,
  123. authorName: item.annotation.account.name,
  124. logAnnotation: item.annotation,
  125. created_at: 0,
  126. }
  127. }
  128. return undefined
  129. })(),
  130. parentMessageId: `question-${item.id}`,
  131. })
  132. })
  133. return newChatList
  134. }
  135. type IDetailPanel = {
  136. detail: any
  137. onFeedback: FeedbackFunc
  138. onSubmitAnnotation: SubmitAnnotationFunc
  139. }
  140. function DetailPanel({ detail, onFeedback }: IDetailPanel) {
  141. const { userProfile: { timezone } } = useAppContext()
  142. const { formatTime } = useTimestamp()
  143. const { onClose, appDetail } = useContext(DrawerContext)
  144. const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, showPromptLogModal, setShowPromptLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
  145. currentLogItem: state.currentLogItem,
  146. setCurrentLogItem: state.setCurrentLogItem,
  147. showMessageLogModal: state.showMessageLogModal,
  148. setShowMessageLogModal: state.setShowMessageLogModal,
  149. showPromptLogModal: state.showPromptLogModal,
  150. setShowPromptLogModal: state.setShowPromptLogModal,
  151. currentLogModalActiveTab: state.currentLogModalActiveTab,
  152. })))
  153. const { t } = useTranslation()
  154. const [hasMore, setHasMore] = useState(true)
  155. const [varValues, setVarValues] = useState<Record<string, string>>({})
  156. const [allChatItems, setAllChatItems] = useState<IChatItem[]>([])
  157. const [chatItemTree, setChatItemTree] = useState<ChatItemInTree[]>([])
  158. const [threadChatItems, setThreadChatItems] = useState<IChatItem[]>([])
  159. const fetchData = useCallback(async () => {
  160. try {
  161. if (!hasMore)
  162. return
  163. const params: ChatMessagesRequest = {
  164. conversation_id: detail.id,
  165. limit: 10,
  166. }
  167. if (allChatItems[0]?.id)
  168. params.first_id = allChatItems[0]?.id.replace('question-', '')
  169. const messageRes = await fetchChatMessages({
  170. url: `/apps/${appDetail?.id}/chat-messages`,
  171. params,
  172. })
  173. if (messageRes.data.length > 0) {
  174. const varValues = messageRes.data.at(-1)!.inputs
  175. setVarValues(varValues)
  176. }
  177. setHasMore(messageRes.has_more)
  178. const newAllChatItems = [
  179. ...getFormattedChatList(messageRes.data, detail.id, timezone!, t('appLog.dateTimeFormat') as string),
  180. ...allChatItems,
  181. ]
  182. setAllChatItems(newAllChatItems)
  183. let tree = buildChatItemTree(newAllChatItems)
  184. if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
  185. tree = [{
  186. id: 'introduction',
  187. isAnswer: true,
  188. isOpeningStatement: true,
  189. content: detail?.model_config?.configs?.introduction ?? 'hello',
  190. feedbackDisabled: true,
  191. children: tree,
  192. }]
  193. }
  194. setChatItemTree(tree)
  195. setThreadChatItems(getThreadMessages(tree, newAllChatItems.at(-1)?.id))
  196. }
  197. catch (err) {
  198. console.error(err)
  199. }
  200. }, [allChatItems, detail.id, hasMore, timezone, t, appDetail, detail?.model_config?.configs?.introduction])
  201. const switchSibling = useCallback((siblingMessageId: string) => {
  202. setThreadChatItems(getThreadMessages(chatItemTree, siblingMessageId))
  203. }, [chatItemTree])
  204. const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
  205. setAllChatItems(allChatItems.map((item, i) => {
  206. if (i === index - 1) {
  207. return {
  208. ...item,
  209. content: query,
  210. }
  211. }
  212. if (i === index) {
  213. return {
  214. ...item,
  215. annotation: {
  216. ...item.annotation,
  217. logAnnotation: {
  218. ...item.annotation?.logAnnotation,
  219. content: answer,
  220. },
  221. } as any,
  222. }
  223. }
  224. return item
  225. }))
  226. }, [allChatItems])
  227. const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => {
  228. setAllChatItems(allChatItems.map((item, i) => {
  229. if (i === index - 1) {
  230. return {
  231. ...item,
  232. content: query,
  233. }
  234. }
  235. if (i === index) {
  236. const answerItem = {
  237. ...item,
  238. content: item.content,
  239. annotation: {
  240. id: annotationId,
  241. authorName,
  242. logAnnotation: {
  243. content: answer,
  244. account: {
  245. id: '',
  246. name: authorName,
  247. email: '',
  248. },
  249. },
  250. } as Annotation,
  251. }
  252. return answerItem
  253. }
  254. return item
  255. }))
  256. }, [allChatItems])
  257. const handleAnnotationRemoved = useCallback((index: number) => {
  258. setAllChatItems(allChatItems.map((item, i) => {
  259. if (i === index) {
  260. return {
  261. ...item,
  262. content: item.content,
  263. annotation: undefined,
  264. }
  265. }
  266. return item
  267. }))
  268. }, [allChatItems])
  269. const fetchInitiated = useRef(false)
  270. useEffect(() => {
  271. if (appDetail?.id && detail.id && appDetail?.mode !== 'completion' && !fetchInitiated.current) {
  272. fetchInitiated.current = true
  273. fetchData()
  274. }
  275. }, [appDetail?.id, detail.id, appDetail?.mode, fetchData])
  276. const isChatMode = appDetail?.mode !== 'completion'
  277. const isAdvanced = appDetail?.mode === 'advanced-chat'
  278. const varList = (detail.model_config as any).user_input_form?.map((item: any) => {
  279. const itemContent = item[Object.keys(item)[0]]
  280. return {
  281. label: itemContent.variable,
  282. value: varValues[itemContent.variable] || detail.message?.inputs?.[itemContent.variable],
  283. }
  284. }) || []
  285. const message_files = (!isChatMode && detail.message.message_files && detail.message.message_files.length > 0)
  286. ? detail.message.message_files.map((item: any) => item.url)
  287. : []
  288. const [width, setWidth] = useState(0)
  289. const ref = useRef<HTMLDivElement>(null)
  290. const adjustModalWidth = () => {
  291. if (ref.current)
  292. setWidth(document.body.clientWidth - (ref.current?.clientWidth + 16) - 8)
  293. }
  294. useEffect(() => {
  295. adjustModalWidth()
  296. }, [])
  297. return (
  298. <div ref={ref} className='rounded-xl border-[0.5px] border-components-panel-border h-full flex flex-col'>
  299. {/* Panel Header */}
  300. <div className='shrink-0 pl-4 pt-3 pr-3 pb-2 flex items-center gap-2 bg-components-panel-bg rounded-t-xl'>
  301. <div className='shrink-0'>
  302. <div className='mb-0.5 text-text-primary system-xs-semibold-uppercase'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</div>
  303. {isChatMode && (
  304. <div className='flex items-center text-text-secondary system-2xs-regular-uppercase'>
  305. <Tooltip
  306. popupContent={detail.id}
  307. >
  308. <div className='truncate'>{detail.id}</div>
  309. </Tooltip>
  310. <CopyIcon content={detail.id} />
  311. </div>
  312. )}
  313. {!isChatMode && (
  314. <div className='text-text-secondary system-2xs-regular-uppercase'>{formatTime(detail.created_at, t('appLog.dateTimeFormat') as string)}</div>
  315. )}
  316. </div>
  317. <div className='grow flex items-center flex-wrap gap-y-1 justify-end'>
  318. {!isAdvanced && <ModelInfo model={detail.model_config.model} />}
  319. </div>
  320. <ActionButton size='l' onClick={onClose}>
  321. <RiCloseLine className='w-4 h-4 text-text-tertiary' />
  322. </ActionButton>
  323. </div>
  324. {/* Panel Body */}
  325. <div className='shrink-0 pt-1 px-1'>
  326. <div className='p-3 pb-2 rounded-t-xl bg-background-section-burn'>
  327. {(varList.length > 0 || (!isChatMode && message_files.length > 0)) && (
  328. <VarPanel
  329. varList={varList}
  330. message_files={message_files}
  331. />
  332. )}
  333. </div>
  334. </div>
  335. <div className='grow mx-1 mb-1 bg-background-section-burn rounded-b-xl overflow-auto'>
  336. {!isChatMode
  337. ? <div className="px-6 py-4">
  338. <div className='flex h-[18px] items-center space-x-3'>
  339. <div className='text-text-tertiary system-xs-semibold-uppercase'>{t('appLog.table.header.output')}</div>
  340. <div className='grow h-[1px]' style={{
  341. background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, rgb(243, 244, 246) 100%)',
  342. }}></div>
  343. </div>
  344. <TextGeneration
  345. className='mt-2'
  346. content={detail.message.answer}
  347. messageId={detail.message.id}
  348. isError={false}
  349. onRetry={() => { }}
  350. isInstalledApp={false}
  351. supportFeedback
  352. feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
  353. onFeedback={feedback => onFeedback(detail.message.id, feedback)}
  354. supportAnnotation
  355. isShowTextToSpeech
  356. appId={appDetail?.id}
  357. varList={varList}
  358. siteInfo={null}
  359. />
  360. </div>
  361. : threadChatItems.length < 8
  362. ? <div className="pt-4 mb-4">
  363. <Chat
  364. config={{
  365. appId: appDetail?.id,
  366. text_to_speech: {
  367. enabled: true,
  368. },
  369. supportAnnotation: true,
  370. annotation_reply: {
  371. enabled: true,
  372. },
  373. supportFeedback: true,
  374. } as any}
  375. chatList={threadChatItems}
  376. onAnnotationAdded={handleAnnotationAdded}
  377. onAnnotationEdited={handleAnnotationEdited}
  378. onAnnotationRemoved={handleAnnotationRemoved}
  379. onFeedback={onFeedback}
  380. noChatInput
  381. showPromptLog
  382. hideProcessDetail
  383. chatContainerInnerClassName='px-3'
  384. switchSibling={switchSibling}
  385. />
  386. </div>
  387. : <div
  388. className="py-4"
  389. id="scrollableDiv"
  390. style={{
  391. height: 1000, // Specify a value
  392. overflow: 'auto',
  393. display: 'flex',
  394. flexDirection: 'column-reverse',
  395. }}>
  396. {/* Put the scroll bar always on the bottom */}
  397. <InfiniteScroll
  398. scrollableTarget="scrollableDiv"
  399. dataLength={threadChatItems.length}
  400. next={fetchData}
  401. hasMore={hasMore}
  402. loader={<div className='text-center text-text-tertiary system-xs-regular'>{t('appLog.detail.loading')}...</div>}
  403. // endMessage={<div className='text-center'>Nothing more to show</div>}
  404. // below props only if you need pull down functionality
  405. refreshFunction={fetchData}
  406. pullDownToRefresh
  407. pullDownToRefreshThreshold={50}
  408. // pullDownToRefreshContent={
  409. // <div className='text-center'>Pull down to refresh</div>
  410. // }
  411. // releaseToRefreshContent={
  412. // <div className='text-center'>Release to refresh</div>
  413. // }
  414. // To put endMessage and loader to the top.
  415. style={{ display: 'flex', flexDirection: 'column-reverse' }}
  416. inverse={true}
  417. >
  418. <Chat
  419. config={{
  420. appId: appDetail?.id,
  421. text_to_speech: {
  422. enabled: true,
  423. },
  424. supportAnnotation: true,
  425. annotation_reply: {
  426. enabled: true,
  427. },
  428. supportFeedback: true,
  429. } as any}
  430. chatList={threadChatItems}
  431. onAnnotationAdded={handleAnnotationAdded}
  432. onAnnotationEdited={handleAnnotationEdited}
  433. onAnnotationRemoved={handleAnnotationRemoved}
  434. onFeedback={onFeedback}
  435. noChatInput
  436. showPromptLog
  437. hideProcessDetail
  438. chatContainerInnerClassName='px-3'
  439. switchSibling={switchSibling}
  440. />
  441. </InfiniteScroll>
  442. </div>
  443. }
  444. </div>
  445. {showMessageLogModal && (
  446. <MessageLogModal
  447. width={width}
  448. currentLogItem={currentLogItem}
  449. onCancel={() => {
  450. setCurrentLogItem()
  451. setShowMessageLogModal(false)
  452. }}
  453. defaultTab={currentLogModalActiveTab}
  454. />
  455. )}
  456. {showPromptLogModal && (
  457. <PromptLogModal
  458. width={width}
  459. currentLogItem={currentLogItem}
  460. onCancel={() => {
  461. setCurrentLogItem()
  462. setShowPromptLogModal(false)
  463. }}
  464. />
  465. )}
  466. </div>
  467. )
  468. }
  469. /**
  470. * Text App Conversation Detail Component
  471. */
  472. const CompletionConversationDetailComp: FC<{ appId?: string; conversationId?: string }> = ({ appId, conversationId }) => {
  473. // Text Generator App Session Details Including Message List
  474. const detailParams = ({ url: `/apps/${appId}/completion-conversations/${conversationId}` })
  475. const { data: conversationDetail, mutate: conversationDetailMutate } = useSWR(() => (appId && conversationId) ? detailParams : null, fetchCompletionConversationDetail)
  476. const { notify } = useContext(ToastContext)
  477. const { t } = useTranslation()
  478. const handleFeedback = async (mid: string, { rating }: FeedbackType): Promise<boolean> => {
  479. try {
  480. await updateLogMessageFeedbacks({ url: `/apps/${appId}/feedbacks`, body: { message_id: mid, rating } })
  481. conversationDetailMutate()
  482. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  483. return true
  484. }
  485. catch (err) {
  486. notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
  487. return false
  488. }
  489. }
  490. const handleAnnotation = async (mid: string, value: string): Promise<boolean> => {
  491. try {
  492. await updateLogMessageAnnotations({ url: `/apps/${appId}/annotations`, body: { message_id: mid, content: value } })
  493. conversationDetailMutate()
  494. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  495. return true
  496. }
  497. catch (err) {
  498. notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
  499. return false
  500. }
  501. }
  502. if (!conversationDetail)
  503. return null
  504. return <DetailPanel
  505. detail={conversationDetail}
  506. onFeedback={handleFeedback}
  507. onSubmitAnnotation={handleAnnotation}
  508. />
  509. }
  510. /**
  511. * Chat App Conversation Detail Component
  512. */
  513. const ChatConversationDetailComp: FC<{ appId?: string; conversationId?: string }> = ({ appId, conversationId }) => {
  514. const detailParams = { url: `/apps/${appId}/chat-conversations/${conversationId}` }
  515. const { data: conversationDetail } = useSWR(() => (appId && conversationId) ? detailParams : null, fetchChatConversationDetail)
  516. const { notify } = useContext(ToastContext)
  517. const { t } = useTranslation()
  518. const handleFeedback = async (mid: string, { rating }: FeedbackType): Promise<boolean> => {
  519. try {
  520. await updateLogMessageFeedbacks({ url: `/apps/${appId}/feedbacks`, body: { message_id: mid, rating } })
  521. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  522. return true
  523. }
  524. catch (err) {
  525. notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
  526. return false
  527. }
  528. }
  529. const handleAnnotation = async (mid: string, value: string): Promise<boolean> => {
  530. try {
  531. await updateLogMessageAnnotations({ url: `/apps/${appId}/annotations`, body: { message_id: mid, content: value } })
  532. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  533. return true
  534. }
  535. catch (err) {
  536. notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
  537. return false
  538. }
  539. }
  540. if (!conversationDetail)
  541. return null
  542. return <DetailPanel
  543. detail={conversationDetail}
  544. onFeedback={handleFeedback}
  545. onSubmitAnnotation={handleAnnotation}
  546. />
  547. }
  548. /**
  549. * Conversation list component including basic information
  550. */
  551. const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh }) => {
  552. const { t } = useTranslation()
  553. const { formatTime } = useTimestamp()
  554. const media = useBreakpoints()
  555. const isMobile = media === MediaType.mobile
  556. const [showDrawer, setShowDrawer] = useState<boolean>(false) // Whether to display the chat details drawer
  557. const [currentConversation, setCurrentConversation] = useState<ChatConversationGeneralDetail | CompletionConversationGeneralDetail | undefined>() // Currently selected conversation
  558. const isChatMode = appDetail.mode !== 'completion' // Whether the app is a chat app
  559. const { setShowPromptLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
  560. setShowPromptLogModal: state.setShowPromptLogModal,
  561. setShowAgentLogModal: state.setShowAgentLogModal,
  562. })))
  563. // Annotated data needs to be highlighted
  564. const renderTdValue = (value: string | number | null, isEmptyStyle: boolean, isHighlight = false, annotation?: LogAnnotation) => {
  565. return (
  566. <Tooltip
  567. popupContent={
  568. <span className='text-xs text-text-tertiary inline-flex items-center'>
  569. <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')}`}
  570. </span>
  571. }
  572. popupClassName={(isHighlight && !isChatMode) ? '' : '!hidden'}
  573. >
  574. <div className={cn(isEmptyStyle ? 'text-text-quaternary' : 'text-text-secondary', !isHighlight ? '' : 'bg-orange-100', 'system-sm-regular overflow-hidden text-ellipsis whitespace-nowrap')}>
  575. {value || '-'}
  576. </div>
  577. </Tooltip>
  578. )
  579. }
  580. const onCloseDrawer = () => {
  581. onRefresh()
  582. setShowDrawer(false)
  583. setCurrentConversation(undefined)
  584. setShowPromptLogModal(false)
  585. setShowAgentLogModal(false)
  586. }
  587. if (!logs)
  588. return <Loading />
  589. return (
  590. <div className='overflow-x-auto'>
  591. <table className={cn('mt-2 w-full min-w-[440px] border-collapse border-0')}>
  592. <thead className='system-xs-medium-uppercase text-text-tertiary'>
  593. <tr>
  594. <td className='pl-2 pr-1 w-5 rounded-l-lg bg-background-section-burn whitespace-nowrap'></td>
  595. <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{isChatMode ? t('appLog.table.header.summary') : t('appLog.table.header.input')}</td>
  596. <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.endUser')}</td>
  597. <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{isChatMode ? t('appLog.table.header.messageCount') : t('appLog.table.header.output')}</td>
  598. <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.userRate')}</td>
  599. <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.adminRate')}</td>
  600. <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.updatedTime')}</td>
  601. <td className='pl-3 py-1.5 rounded-r-lg bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.time')}</td>
  602. </tr>
  603. </thead>
  604. <tbody className="text-text-secondary system-sm-regular">
  605. {logs.data.map((log: any) => {
  606. const endUser = log.from_end_user_session_id || log.from_account_name
  607. const leftValue = get(log, isChatMode ? 'name' : 'message.inputs.query') || (!isChatMode ? (get(log, 'message.query') || get(log, 'message.inputs.default_input')) : '') || ''
  608. const rightValue = get(log, isChatMode ? 'message_count' : 'message.answer')
  609. return <tr
  610. key={log.id}
  611. className={cn('border-b border-divider-subtle hover:bg-background-default-hover cursor-pointer', currentConversation?.id !== log.id ? '' : 'bg-background-default-hover')}
  612. onClick={() => {
  613. setShowDrawer(true)
  614. setCurrentConversation(log)
  615. }}>
  616. <td className='h-4'>
  617. {!log.read_at && (
  618. <div className='p-3 pr-0.5 flex items-center'>
  619. <span className='inline-block bg-util-colors-blue-blue-500 h-1.5 w-1.5 rounded'></span>
  620. </div>
  621. )}
  622. </td>
  623. <td className='p-3 pr-2 w-[160px]' style={{ maxWidth: isChatMode ? 300 : 200 }}>
  624. {renderTdValue(leftValue || t('appLog.table.empty.noChat'), !leftValue, isChatMode && log.annotated)}
  625. </td>
  626. <td className='p-3 pr-2'>{renderTdValue(endUser || defaultValue, !endUser)}</td>
  627. <td className='p-3 pr-2' style={{ maxWidth: isChatMode ? 100 : 200 }}>
  628. {renderTdValue(rightValue === 0 ? 0 : (rightValue || t('appLog.table.empty.noOutput')), !rightValue, !isChatMode && !!log.annotation?.content, log.annotation)}
  629. </td>
  630. <td className='p-3 pr-2'>
  631. {(!log.user_feedback_stats.like && !log.user_feedback_stats.dislike)
  632. ? renderTdValue(defaultValue, true)
  633. : <>
  634. {!!log.user_feedback_stats.like && <HandThumbIconWithCount iconType='up' count={log.user_feedback_stats.like} />}
  635. {!!log.user_feedback_stats.dislike && <HandThumbIconWithCount iconType='down' count={log.user_feedback_stats.dislike} />}
  636. </>
  637. }
  638. </td>
  639. <td className='p-3 pr-2'>
  640. {(!log.admin_feedback_stats.like && !log.admin_feedback_stats.dislike)
  641. ? renderTdValue(defaultValue, true)
  642. : <>
  643. {!!log.admin_feedback_stats.like && <HandThumbIconWithCount iconType='up' count={log.admin_feedback_stats.like} />}
  644. {!!log.admin_feedback_stats.dislike && <HandThumbIconWithCount iconType='down' count={log.admin_feedback_stats.dislike} />}
  645. </>
  646. }
  647. </td>
  648. <td className='w-[160px] p-3 pr-2'>{formatTime(log.updated_at, t('appLog.dateTimeFormat') as string)}</td>
  649. <td className='w-[160px] p-3 pr-2'>{formatTime(log.created_at, t('appLog.dateTimeFormat') as string)}</td>
  650. </tr>
  651. })}
  652. </tbody>
  653. </table>
  654. <Drawer
  655. isOpen={showDrawer}
  656. onClose={onCloseDrawer}
  657. mask={isMobile}
  658. footer={null}
  659. panelClassname='mt-16 mx-2 sm:mr-2 mb-4 !p-0 !max-w-[640px] rounded-xl bg-components-panel-bg'
  660. >
  661. <DrawerContext.Provider value={{
  662. onClose: onCloseDrawer,
  663. appDetail,
  664. }}>
  665. {isChatMode
  666. ? <ChatConversationDetailComp appId={appDetail.id} conversationId={currentConversation?.id} />
  667. : <CompletionConversationDetailComp appId={appDetail.id} conversationId={currentConversation?.id} />
  668. }
  669. </DrawerContext.Provider>
  670. </Drawer>
  671. </div>
  672. )
  673. }
  674. export default ConversationList