list.tsx 31 KB

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