list.tsx 29 KB

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