list.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { useEffect, useState } from 'react'
  4. import useSWR from 'swr'
  5. import {
  6. HandThumbDownIcon,
  7. HandThumbUpIcon,
  8. InformationCircleIcon,
  9. XMarkIcon,
  10. } from '@heroicons/react/24/outline'
  11. import { get } from 'lodash-es'
  12. import InfiniteScroll from 'react-infinite-scroll-component'
  13. import dayjs from 'dayjs'
  14. import { createContext, useContext } from 'use-context-selector'
  15. import { useTranslation } from 'react-i18next'
  16. import cn from 'classnames'
  17. import s from './style.module.css'
  18. import VarPanel from './var-panel'
  19. import { randomString } from '@/utils'
  20. import { EditIconSolid } from '@/app/components/app/chat/icon-component'
  21. import type { FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc } from '@/app/components/app/chat/type'
  22. import type { ChatConversationFullDetailResponse, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationFullDetailResponse, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log'
  23. import type { App } from '@/types/app'
  24. import Loading from '@/app/components/base/loading'
  25. import Drawer from '@/app/components/base/drawer'
  26. import Popover from '@/app/components/base/popover'
  27. import Chat from '@/app/components/app/chat'
  28. import Tooltip from '@/app/components/base/tooltip'
  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. type IConversationList = {
  39. logs?: ChatConversationsResponse | CompletionConversationsResponse
  40. appDetail?: App
  41. onRefresh: () => void
  42. }
  43. const defaultValue = 'N/A'
  44. type IDrawerContext = {
  45. onClose: () => void
  46. appDetail?: App
  47. }
  48. const DrawerContext = createContext<IDrawerContext>({} as IDrawerContext)
  49. /**
  50. * Icon component with numbers
  51. */
  52. const HandThumbIconWithCount: FC<{ count: number; iconType: 'up' | 'down' }> = ({ count, iconType }) => {
  53. const classname = iconType === 'up' ? 'text-primary-600 bg-primary-50' : 'text-red-600 bg-red-50'
  54. const Icon = iconType === 'up' ? HandThumbUpIcon : HandThumbDownIcon
  55. return <div className={`inline-flex items-center w-fit rounded-md p-1 text-xs ${classname} mr-1 last:mr-0`}>
  56. <Icon className={'h-3 w-3 mr-0.5 rounded-md'} />
  57. {count > 0 ? count : null}
  58. </div>
  59. }
  60. const PARAM_MAP = {
  61. temperature: 'Temperature',
  62. top_p: 'Top P',
  63. presence_penalty: 'Presence Penalty',
  64. max_tokens: 'Max Token',
  65. stop: 'Stop',
  66. frequency_penalty: 'Frequency Penalty',
  67. }
  68. // Format interface data for easy display
  69. const getFormattedChatList = (messages: ChatMessage[]) => {
  70. const newChatList: IChatItem[] = []
  71. messages.forEach((item: ChatMessage) => {
  72. newChatList.push({
  73. id: `question-${item.id}`,
  74. content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
  75. isAnswer: false,
  76. log: item.message as any,
  77. message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
  78. })
  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: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
  88. more: {
  89. time: dayjs.unix(item.created_at).format('hh:mm A'),
  90. tokens: item.answer_tokens + item.message_tokens,
  91. latency: item.provider_response_latency.toFixed(2),
  92. },
  93. annotation: (() => {
  94. if (item.annotation_hit_history) {
  95. return {
  96. id: item.annotation_hit_history.annotation_id,
  97. authorName: item.annotation_hit_history.annotation_create_account?.name || 'N/A',
  98. created_at: item.annotation_hit_history.created_at,
  99. }
  100. }
  101. if (item.annotation) {
  102. return {
  103. id: '',
  104. authorName: '',
  105. logAnnotation: item.annotation,
  106. created_at: 0,
  107. }
  108. }
  109. return undefined
  110. })(),
  111. })
  112. })
  113. return newChatList
  114. }
  115. // const displayedParams = CompletionParams.slice(0, -2)
  116. const validatedParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty']
  117. type IDetailPanel<T> = {
  118. detail: any
  119. onFeedback: FeedbackFunc
  120. onSubmitAnnotation: SubmitAnnotationFunc
  121. }
  122. function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionConversationFullDetailResponse>({ detail, onFeedback }: IDetailPanel<T>) {
  123. const { onClose, appDetail } = useContext(DrawerContext)
  124. const { t } = useTranslation()
  125. const [items, setItems] = React.useState<IChatItem[]>([])
  126. const [hasMore, setHasMore] = useState(true)
  127. const [varValues, setVarValues] = useState<Record<string, string>>({})
  128. const fetchData = async () => {
  129. try {
  130. if (!hasMore)
  131. return
  132. const params: ChatMessagesRequest = {
  133. conversation_id: detail.id,
  134. limit: 4,
  135. }
  136. if (items?.[0]?.id)
  137. params.first_id = items?.[0]?.id.replace('question-', '')
  138. const messageRes = await fetchChatMessages({
  139. url: `/apps/${appDetail?.id}/chat-messages`,
  140. params,
  141. })
  142. if (messageRes.data.length > 0) {
  143. const varValues = messageRes.data[0].inputs
  144. setVarValues(varValues)
  145. }
  146. const newItems = [...getFormattedChatList(messageRes.data), ...items]
  147. if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
  148. newItems.unshift({
  149. id: 'introduction',
  150. isAnswer: true,
  151. isOpeningStatement: true,
  152. content: detail?.model_config?.configs?.introduction ?? 'hello',
  153. feedbackDisabled: true,
  154. })
  155. }
  156. setItems(newItems)
  157. setHasMore(messageRes.has_more)
  158. }
  159. catch (err) {
  160. console.error(err)
  161. }
  162. }
  163. useEffect(() => {
  164. if (appDetail?.id && detail.id && appDetail?.mode === 'chat')
  165. fetchData()
  166. }, [appDetail?.id, detail.id, appDetail?.mode])
  167. const isChatMode = appDetail?.mode === 'chat'
  168. const targetTone = TONE_LIST.find((item: any) => {
  169. let res = true
  170. validatedParams.forEach((param) => {
  171. res = item.config?.[param] === detail.model_config?.configs?.completion_params?.[param]
  172. })
  173. return res
  174. })?.name ?? 'custom'
  175. const modelName = (detail.model_config as any).model.name
  176. const provideName = (detail.model_config as any).model.provider as any
  177. const {
  178. currentModel,
  179. currentProvider,
  180. } = useTextGenerationCurrentProviderAndModelAndModelList(
  181. { provider: provideName, model: modelName },
  182. )
  183. const varList = (detail.model_config as any).user_input_form.map((item: any) => {
  184. const itemContent = item[Object.keys(item)[0]]
  185. return {
  186. label: itemContent.variable,
  187. value: varValues[itemContent.variable] || detail.message?.inputs?.[itemContent.variable],
  188. }
  189. })
  190. const message_files = (!isChatMode && detail.message.message_files && detail.message.message_files.length > 0)
  191. ? detail.message.message_files.map((item: any) => item.url)
  192. : []
  193. const getParamValue = (param: string) => {
  194. const value = detail?.model_config.model?.completion_params?.[param] || '-'
  195. if (param === 'stop') {
  196. if (Array.isArray(value))
  197. return value.join(',')
  198. else
  199. return '-'
  200. }
  201. return value
  202. }
  203. return (<div className='rounded-xl border-[0.5px] border-gray-200 h-full flex flex-col overflow-auto'>
  204. {/* Panel Header */}
  205. <div className='border-b border-gray-100 py-4 px-6 flex items-center justify-between'>
  206. <div>
  207. <div className='text-gray-500 text-[10px] leading-[14px]'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</div>
  208. <div className='text-gray-700 text-[13px] leading-[18px]'>{isChatMode ? detail.id?.split('-').slice(-1)[0] : dayjs.unix(detail.created_at).format(t('appLog.dateTimeFormat') as string)}</div>
  209. </div>
  210. <div className='flex items-center flex-wrap gap-y-1 justify-end'>
  211. <div
  212. className={cn('mr-2 flex items-center border h-8 px-2 space-x-2 rounded-lg bg-indigo-25 border-[#2A87F5]')}
  213. >
  214. <ModelIcon
  215. className='!w-5 !h-5'
  216. provider={currentProvider}
  217. modelName={currentModel?.model}
  218. />
  219. <ModelName
  220. modelItem={currentModel!}
  221. showMode
  222. />
  223. </div>
  224. <Popover
  225. position='br'
  226. className='!w-[280px]'
  227. btnClassName='mr-4 !bg-gray-50 !py-1.5 !px-2.5 border-none font-normal'
  228. btnElement={<>
  229. <span className='text-[13px]'>{targetTone}</span>
  230. <InformationCircleIcon className='h-4 w-4 text-gray-800 ml-1.5' />
  231. </>}
  232. htmlContent={<div className='w-[280px]'>
  233. <div className='flex justify-between py-2 px-4 font-medium text-sm text-gray-700'>
  234. <span>Tone of responses</span>
  235. <div>{targetTone}</div>
  236. </div>
  237. {['temperature', 'top_p', 'presence_penalty', 'max_tokens', 'stop'].map((param: string, index: number) => {
  238. return <div className='flex justify-between py-2 px-4 bg-gray-50' key={index}>
  239. <span className='text-xs text-gray-700'>{PARAM_MAP[param as keyof typeof PARAM_MAP]}</span>
  240. <span className='text-gray-800 font-medium text-xs'>{getParamValue(param)}</span>
  241. </div>
  242. })}
  243. </div>}
  244. />
  245. <div className='w-6 h-6 rounded-lg flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
  246. <XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
  247. </div>
  248. </div>
  249. </div>
  250. {/* Panel Body */}
  251. {(varList.length > 0 || (!isChatMode && message_files.length > 0)) && (
  252. <div className='px-6 pt-4 pb-2'>
  253. <VarPanel
  254. varList={varList}
  255. message_files={message_files}
  256. />
  257. </div>
  258. )}
  259. {!isChatMode
  260. ? <div className="px-6 py-4">
  261. <div className='flex h-[18px] items-center space-x-3'>
  262. <div className='leading-[18px] text-xs font-semibold text-gray-500 uppercase'>{t('appLog.table.header.output')}</div>
  263. <div className='grow h-[1px]' style={{
  264. background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, rgb(243, 244, 246) 100%)',
  265. }}></div>
  266. </div>
  267. <TextGeneration
  268. className='mt-2'
  269. content={detail.message.answer}
  270. messageId={detail.message.id}
  271. isError={false}
  272. onRetry={() => { }}
  273. isInstalledApp={false}
  274. supportFeedback
  275. feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
  276. onFeedback={feedback => onFeedback(detail.message.id, feedback)}
  277. supportAnnotation
  278. appId={appDetail?.id}
  279. varList={varList}
  280. />
  281. </div>
  282. : items.length < 8
  283. ? <div className="px-2.5 pt-4 mb-4">
  284. <Chat
  285. chatList={items}
  286. isHideSendInput={true}
  287. onFeedback={onFeedback}
  288. displayScene='console'
  289. isShowPromptLog
  290. supportAnnotation
  291. appId={appDetail?.id}
  292. onChatListChange={setItems}
  293. />
  294. </div>
  295. : <div
  296. className="px-2.5 py-4"
  297. id="scrollableDiv"
  298. style={{
  299. height: 1000, // Specify a value
  300. overflow: 'auto',
  301. display: 'flex',
  302. flexDirection: 'column-reverse',
  303. }}>
  304. {/* Put the scroll bar always on the bottom */}
  305. <InfiniteScroll
  306. scrollableTarget="scrollableDiv"
  307. dataLength={items.length}
  308. next={fetchData}
  309. hasMore={hasMore}
  310. loader={<div className='text-center text-gray-400 text-xs'>{t('appLog.detail.loading')}...</div>}
  311. // endMessage={<div className='text-center'>Nothing more to show</div>}
  312. // below props only if you need pull down functionality
  313. refreshFunction={fetchData}
  314. pullDownToRefresh
  315. pullDownToRefreshThreshold={50}
  316. // pullDownToRefreshContent={
  317. // <div className='text-center'>Pull down to refresh</div>
  318. // }
  319. // releaseToRefreshContent={
  320. // <div className='text-center'>Release to refresh</div>
  321. // }
  322. // To put endMessage and loader to the top.
  323. style={{ display: 'flex', flexDirection: 'column-reverse' }}
  324. inverse={true}
  325. >
  326. <Chat
  327. chatList={items}
  328. isHideSendInput={true}
  329. onFeedback={onFeedback}
  330. displayScene='console'
  331. isShowPromptLog
  332. />
  333. </InfiniteScroll>
  334. </div>
  335. }
  336. </div>)
  337. }
  338. /**
  339. * Text App Conversation Detail Component
  340. */
  341. const CompletionConversationDetailComp: FC<{ appId?: string; conversationId?: string }> = ({ appId, conversationId }) => {
  342. // Text Generator App Session Details Including Message List
  343. const detailParams = ({ url: `/apps/${appId}/completion-conversations/${conversationId}` })
  344. const { data: conversationDetail, mutate: conversationDetailMutate } = useSWR(() => (appId && conversationId) ? detailParams : null, fetchCompletionConversationDetail)
  345. const { notify } = useContext(ToastContext)
  346. const { t } = useTranslation()
  347. const handleFeedback = async (mid: string, { rating }: Feedbacktype): Promise<boolean> => {
  348. try {
  349. await updateLogMessageFeedbacks({ url: `/apps/${appId}/feedbacks`, body: { message_id: mid, rating } })
  350. conversationDetailMutate()
  351. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  352. return true
  353. }
  354. catch (err) {
  355. notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
  356. return false
  357. }
  358. }
  359. const handleAnnotation = async (mid: string, value: string): Promise<boolean> => {
  360. try {
  361. await updateLogMessageAnnotations({ url: `/apps/${appId}/annotations`, body: { message_id: mid, content: value } })
  362. conversationDetailMutate()
  363. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  364. return true
  365. }
  366. catch (err) {
  367. notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
  368. return false
  369. }
  370. }
  371. if (!conversationDetail)
  372. return null
  373. return <DetailPanel<CompletionConversationFullDetailResponse>
  374. detail={conversationDetail}
  375. onFeedback={handleFeedback}
  376. onSubmitAnnotation={handleAnnotation}
  377. />
  378. }
  379. /**
  380. * Chat App Conversation Detail Component
  381. */
  382. const ChatConversationDetailComp: FC<{ appId?: string; conversationId?: string }> = ({ appId, conversationId }) => {
  383. const detailParams = { url: `/apps/${appId}/chat-conversations/${conversationId}` }
  384. const { data: conversationDetail } = useSWR(() => (appId && conversationId) ? detailParams : null, fetchChatConversationDetail)
  385. const { notify } = useContext(ToastContext)
  386. const { t } = useTranslation()
  387. const handleFeedback = async (mid: string, { rating }: Feedbacktype): Promise<boolean> => {
  388. try {
  389. await updateLogMessageFeedbacks({ url: `/apps/${appId}/feedbacks`, body: { message_id: mid, rating } })
  390. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  391. return true
  392. }
  393. catch (err) {
  394. notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
  395. return false
  396. }
  397. }
  398. const handleAnnotation = async (mid: string, value: string): Promise<boolean> => {
  399. try {
  400. await updateLogMessageAnnotations({ url: `/apps/${appId}/annotations`, body: { message_id: mid, content: value } })
  401. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  402. return true
  403. }
  404. catch (err) {
  405. notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
  406. return false
  407. }
  408. }
  409. if (!conversationDetail)
  410. return null
  411. return <DetailPanel<ChatConversationFullDetailResponse>
  412. detail={conversationDetail}
  413. onFeedback={handleFeedback}
  414. onSubmitAnnotation={handleAnnotation}
  415. />
  416. }
  417. /**
  418. * Conversation list component including basic information
  419. */
  420. const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh }) => {
  421. const { t } = useTranslation()
  422. const media = useBreakpoints()
  423. const isMobile = media === MediaType.mobile
  424. const [showDrawer, setShowDrawer] = useState<boolean>(false) // Whether to display the chat details drawer
  425. const [currentConversation, setCurrentConversation] = useState<ChatConversationGeneralDetail | CompletionConversationGeneralDetail | undefined>() // Currently selected conversation
  426. const isChatMode = appDetail?.mode === 'chat' // Whether the app is a chat app
  427. // Annotated data needs to be highlighted
  428. const renderTdValue = (value: string | number | null, isEmptyStyle: boolean, isHighlight = false, annotation?: LogAnnotation) => {
  429. return (
  430. <Tooltip
  431. htmlContent={
  432. <span className='text-xs text-gray-500 inline-flex items-center'>
  433. <EditIconSolid className='mr-1' />{`${t('appLog.detail.annotationTip', { user: annotation?.account?.name })} ${dayjs.unix(annotation?.created_at || dayjs().unix()).format('MM-DD hh:mm A')}`}
  434. </span>
  435. }
  436. className={(isHighlight && !isChatMode) ? '' : '!hidden'}
  437. selector={`highlight-${randomString(16)}`}
  438. >
  439. <div className={cn(isEmptyStyle ? 'text-gray-400' : 'text-gray-700', !isHighlight ? '' : 'bg-orange-100', 'text-sm overflow-hidden text-ellipsis whitespace-nowrap')}>
  440. {value || '-'}
  441. </div>
  442. </Tooltip>
  443. )
  444. }
  445. const onCloseDrawer = () => {
  446. onRefresh()
  447. setShowDrawer(false)
  448. setCurrentConversation(undefined)
  449. }
  450. if (!logs)
  451. return <Loading />
  452. return (
  453. <div className='overflow-x-auto'>
  454. <table className={`w-full min-w-[440px] border-collapse border-0 text-sm mt-3 ${s.logTable}`}>
  455. <thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-bold">
  456. <tr>
  457. <td className='w-[1.375rem] whitespace-nowrap'></td>
  458. <td className='whitespace-nowrap'>{t('appLog.table.header.time')}</td>
  459. <td className='whitespace-nowrap'>{t('appLog.table.header.endUser')}</td>
  460. <td className='whitespace-nowrap'>{isChatMode ? t('appLog.table.header.summary') : t('appLog.table.header.input')}</td>
  461. <td className='whitespace-nowrap'>{isChatMode ? t('appLog.table.header.messageCount') : t('appLog.table.header.output')}</td>
  462. <td className='whitespace-nowrap'>{t('appLog.table.header.userRate')}</td>
  463. <td className='whitespace-nowrap'>{t('appLog.table.header.adminRate')}</td>
  464. </tr>
  465. </thead>
  466. <tbody className="text-gray-500">
  467. {logs.data.map((log: any) => {
  468. const endUser = log.from_end_user_session_id
  469. const leftValue = get(log, isChatMode ? 'name' : 'message.inputs.query') || (!isChatMode ? (get(log, 'message.query') || get(log, 'message.inputs.default_input')) : '') || ''
  470. const rightValue = get(log, isChatMode ? 'message_count' : 'message.answer')
  471. return <tr
  472. key={log.id}
  473. className={`border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer ${currentConversation?.id !== log.id ? '' : 'bg-gray-50'}`}
  474. onClick={() => {
  475. setShowDrawer(true)
  476. setCurrentConversation(log)
  477. }}>
  478. <td className='text-center align-middle'>{!log.read_at && <span className='inline-block bg-[#3F83F8] h-1.5 w-1.5 rounded'></span>}</td>
  479. <td className='w-[160px]'>{dayjs.unix(log.created_at).format(t('appLog.dateTimeFormat') as string)}</td>
  480. <td>{renderTdValue(endUser || defaultValue, !endUser)}</td>
  481. <td style={{ maxWidth: isChatMode ? 300 : 200 }}>
  482. {renderTdValue(leftValue || t('appLog.table.empty.noChat'), !leftValue, isChatMode && log.annotated)}
  483. </td>
  484. <td style={{ maxWidth: isChatMode ? 100 : 200 }}>
  485. {renderTdValue(rightValue === 0 ? 0 : (rightValue || t('appLog.table.empty.noOutput')), !rightValue, !isChatMode && !!log.annotation?.content, log.annotation)}
  486. </td>
  487. <td>
  488. {(!log.user_feedback_stats.like && !log.user_feedback_stats.dislike)
  489. ? renderTdValue(defaultValue, true)
  490. : <>
  491. {!!log.user_feedback_stats.like && <HandThumbIconWithCount iconType='up' count={log.user_feedback_stats.like} />}
  492. {!!log.user_feedback_stats.dislike && <HandThumbIconWithCount iconType='down' count={log.user_feedback_stats.dislike} />}
  493. </>
  494. }
  495. </td>
  496. <td>
  497. {(!log.admin_feedback_stats.like && !log.admin_feedback_stats.dislike)
  498. ? renderTdValue(defaultValue, true)
  499. : <>
  500. {!!log.admin_feedback_stats.like && <HandThumbIconWithCount iconType='up' count={log.admin_feedback_stats.like} />}
  501. {!!log.admin_feedback_stats.dislike && <HandThumbIconWithCount iconType='down' count={log.admin_feedback_stats.dislike} />}
  502. </>
  503. }
  504. </td>
  505. </tr>
  506. })}
  507. </tbody>
  508. </table>
  509. <Drawer
  510. isOpen={showDrawer}
  511. onClose={onCloseDrawer}
  512. mask={isMobile}
  513. footer={null}
  514. panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[640px] rounded-xl'
  515. >
  516. <DrawerContext.Provider value={{
  517. onClose: onCloseDrawer,
  518. appDetail,
  519. }}>
  520. {isChatMode
  521. ? <ChatConversationDetailComp appId={appDetail?.id} conversationId={currentConversation?.id} />
  522. : <CompletionConversationDetailComp appId={appDetail?.id} conversationId={currentConversation?.id} />
  523. }
  524. </DrawerContext.Provider>
  525. </Drawer>
  526. </div>
  527. )
  528. }
  529. export default ConversationList