list.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. 'use client'
  2. import type { FC, SVGProps } from 'react'
  3. import React, { useEffect, useState } from 'react'
  4. // import type { Log } from '@/models/log'
  5. import useSWR from 'swr'
  6. import {
  7. HandThumbDownIcon,
  8. HandThumbUpIcon,
  9. InformationCircleIcon,
  10. XMarkIcon,
  11. } from '@heroicons/react/24/outline'
  12. import { SparklesIcon } from '@heroicons/react/24/solid'
  13. import { get } from 'lodash-es'
  14. import InfiniteScroll from 'react-infinite-scroll-component'
  15. import dayjs from 'dayjs'
  16. import { createContext, useContext } from 'use-context-selector'
  17. import classNames from 'classnames'
  18. import { useTranslation } from 'react-i18next'
  19. import s from './style.module.css'
  20. import { randomString } from '@/utils'
  21. import { EditIconSolid } from '@/app/components/app/chat/icon-component'
  22. import type { FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc } from '@/app/components/app/chat/type'
  23. import type { Annotation, ChatConversationFullDetailResponse, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationFullDetailResponse, CompletionConversationGeneralDetail, CompletionConversationsResponse } 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/app/chat'
  29. import Tooltip from '@/app/components/base/tooltip'
  30. import { ToastContext } from '@/app/components/base/toast'
  31. import { fetchChatConversationDetail, fetchChatMessages, fetchCompletionConversationDetail, updateLogMessageAnnotations, updateLogMessageFeedbacks } from '@/service/log'
  32. import { TONE_LIST } from '@/config'
  33. type IConversationList = {
  34. logs?: ChatConversationsResponse | CompletionConversationsResponse
  35. appDetail?: App
  36. onRefresh: () => void
  37. }
  38. const defaultValue = 'N/A'
  39. const emptyText = '[Empty]'
  40. type IDrawerContext = {
  41. onClose: () => void
  42. appDetail?: App
  43. }
  44. const DrawerContext = createContext<IDrawerContext>({} as IDrawerContext)
  45. export const OpenAIIcon = ({ className }: SVGProps<SVGElement>) => {
  46. return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
  47. <rect width="16" height="16" rx="6" fill="black" />
  48. <path d="M13.6553 7.70613C13.7853 7.99625 13.8678 8.30638 13.9016 8.62276C13.9341 8.93913 13.9179 9.25927 13.8503 9.57064C13.7841 9.88202 13.669 10.1809 13.5089 10.456C13.4039 10.6398 13.2801 10.8124 13.1375 10.9712C12.9962 11.1288 12.8387 11.2713 12.6673 11.3964C12.4948 11.5214 12.311 11.6265 12.1159 11.7128C11.922 11.7978 11.7195 11.8628 11.5119 11.9053C11.4143 12.208 11.2693 12.4943 11.0817 12.7519C10.8954 13.0095 10.669 13.2359 10.4114 13.4222C10.1538 13.6098 9.86871 13.7549 9.56608 13.8524C9.26346 13.9512 8.94708 14 8.6282 14C8.41686 14.0012 8.20428 13.9787 7.99669 13.9362C7.79036 13.8924 7.58778 13.8261 7.39395 13.7398C7.20012 13.6536 7.01629 13.546 6.84497 13.421C6.6749 13.2959 6.51734 13.1521 6.37728 12.9933C6.06465 13.0608 5.74452 13.0771 5.42814 13.0446C5.11177 13.0108 4.80164 12.9283 4.51027 12.7982C4.22015 12.6694 3.95129 12.4943 3.71495 12.2805C3.4786 12.0667 3.27727 11.8166 3.11845 11.5414C3.01216 11.3576 2.92462 11.1638 2.85835 10.9625C2.79207 10.7611 2.7483 10.5535 2.72579 10.3422C2.70328 10.1321 2.70453 9.91954 2.72704 9.7082C2.74955 9.49811 2.79582 9.29053 2.8621 9.0892C2.64951 8.85285 2.47444 8.58399 2.34439 8.29387C2.21558 8.0025 2.1318 7.69363 2.09929 7.37725C2.06552 7.06087 2.08303 6.74074 2.14931 6.42936C2.21558 6.11798 2.33063 5.81911 2.4907 5.544C2.59574 5.36017 2.71954 5.18635 2.86085 5.02879C3.00215 4.87122 3.16097 4.72867 3.33229 4.60361C3.50361 4.47856 3.68868 4.37227 3.88251 4.28724C4.07759 4.20095 4.28018 4.13717 4.48776 4.09466C4.5853 3.79078 4.73036 3.50567 4.91669 3.24806C5.10426 2.99046 5.33061 2.76411 5.58821 2.57654C5.84582 2.39021 6.13093 2.24515 6.43356 2.14636C6.73618 2.04882 7.05256 1.9988 7.37144 2.00005C7.58277 1.9988 7.79536 2.02006 8.00295 2.06383C8.21053 2.1076 8.41311 2.17262 8.60694 2.25891C8.80077 2.34644 8.9846 2.45274 9.15592 2.57779C9.32724 2.70409 9.4848 2.84665 9.62486 3.00546C9.93623 2.93919 10.2564 2.92293 10.5727 2.95544C10.8891 2.98796 11.198 3.07174 11.4894 3.20054C11.7795 3.3306 12.0483 3.50442 12.2847 3.71825C12.521 3.93084 12.7224 4.17969 12.8812 4.45605C12.9875 4.63863 13.075 4.83246 13.1413 5.03504C13.2076 5.23637 13.2526 5.44396 13.2738 5.65529C13.2964 5.86663 13.2964 6.07922 13.2726 6.29055C13.2501 6.50189 13.2038 6.70948 13.1375 6.91081C13.3514 7.14715 13.5252 7.41476 13.6553 7.70613ZM9.48855 13.0446C9.76116 12.932 10.0088 12.7657 10.2176 12.5569C10.4264 12.348 10.5928 12.1004 10.7053 11.8266C10.8178 11.554 10.8766 11.2613 10.8766 10.9662V8.17757C10.8758 8.17507 10.875 8.17215 10.8741 8.16882C10.8733 8.16632 10.872 8.16382 10.8704 8.16132C10.8687 8.15882 10.8666 8.15673 10.8641 8.15507C10.8616 8.15256 10.8591 8.1509 10.8566 8.15006L9.84745 7.56733V10.9362C9.84745 10.97 9.84245 11.005 9.83369 11.0375C9.82494 11.0713 9.81243 11.1025 9.79493 11.1325C9.77742 11.1625 9.75741 11.1901 9.7324 11.2138C9.70809 11.238 9.68077 11.2591 9.65112 11.2763L7.26139 12.6557C7.24138 12.6682 7.20762 12.6857 7.19011 12.6957C7.2889 12.7795 7.39645 12.8532 7.50899 12.9183C7.62279 12.9833 7.74034 13.0383 7.86289 13.0833C7.98544 13.1271 8.11174 13.1609 8.23929 13.1834C8.36809 13.2059 8.49815 13.2171 8.6282 13.2171C8.92332 13.2171 9.21594 13.1584 9.48855 13.0446ZM3.79748 11.1513C3.94629 11.4076 4.14262 11.6302 4.37647 11.8103C4.61156 11.9904 4.87792 12.1217 5.16304 12.198C5.44815 12.2742 5.74577 12.2943 6.03839 12.2555C6.33101 12.2167 6.61238 12.1217 6.86873 11.9741L9.28472 10.5798L9.29097 10.5736C9.29264 10.5719 9.29389 10.5694 9.29472 10.566C9.29639 10.5635 9.29764 10.561 9.29847 10.5585V9.38307L6.38228 11.07C6.35227 11.0875 6.32101 11.1 6.2885 11.11C6.25473 11.1188 6.22097 11.1225 6.18595 11.1225C6.15219 11.1225 6.11843 11.1188 6.08466 11.11C6.05215 11.1 6.01964 11.0875 5.98962 11.07L3.5999 9.68944C3.57864 9.67694 3.54738 9.65818 3.52987 9.64692C3.50736 9.77573 3.49611 9.90578 3.49611 10.0358C3.49611 10.1659 3.50861 10.2959 3.53112 10.4247C3.55363 10.5523 3.58864 10.6786 3.63241 10.8011C3.67743 10.9237 3.73245 11.0412 3.79748 11.1538V11.1513ZM3.16972 5.93666C3.02216 6.19301 2.92712 6.47563 2.88836 6.76825C2.84959 7.06087 2.8696 7.35724 2.94588 7.64361C3.02216 7.92872 3.15347 8.19508 3.33354 8.43018C3.51361 8.66402 3.73745 8.86035 3.99256 9.00791L6.40729 10.4035C6.4098 10.4043 6.41271 10.4051 6.41605 10.406H6.4248C6.42814 10.406 6.43105 10.4051 6.43356 10.4035C6.43606 10.4026 6.43856 10.4014 6.44106 10.3997L7.45397 9.81449L4.53778 8.13131C4.50902 8.1138 4.48151 8.09254 4.4565 8.06878C4.43227 8.04447 4.41125 8.01715 4.39397 7.9875C4.37772 7.95748 4.36396 7.92622 4.35521 7.89246C4.34645 7.85994 4.34145 7.82618 4.3427 7.79117V4.95126C4.22015 4.99628 4.10135 5.0513 3.98881 5.11632C3.87626 5.1826 3.76997 5.25763 3.66993 5.34142C3.57114 5.4252 3.4786 5.51774 3.39481 5.61778C3.31103 5.71657 3.23725 5.82411 3.17222 5.93666H3.16972ZM11.4644 7.86745C11.4944 7.88495 11.5219 7.90496 11.5469 7.92997C11.5707 7.95373 11.5919 7.98124 11.6094 8.01126C11.6257 8.04127 11.6394 8.07378 11.6482 8.10629C11.6557 8.14006 11.6607 8.17382 11.6594 8.20884V11.0487C12.0609 10.9012 12.411 10.6423 12.6699 10.3022C12.93 9.96205 13.0863 9.55564 13.1225 9.13046C13.1588 8.70529 13.0738 8.27762 12.8762 7.89871C12.6786 7.51981 12.3772 7.20468 12.0071 6.99209L9.59234 5.59652C9.58984 5.59569 9.58693 5.59485 9.58359 5.59402H9.57484C9.57234 5.59485 9.56942 5.59569 9.56608 5.59652C9.56358 5.59735 9.56108 5.5986 9.55858 5.60027L8.55067 6.18301L11.4669 7.86745H11.4644ZM12.471 6.35433H12.4698V6.35558L12.471 6.35433ZM12.4698 6.35308C12.5423 5.93291 12.4935 5.50023 12.3285 5.10632C12.1646 4.71241 11.8908 4.37352 11.5406 4.12842C11.1905 3.88457 10.7778 3.74451 10.3514 3.72576C9.92373 3.70825 9.50106 3.81204 9.13091 4.02463L6.71617 5.41895C6.71367 5.42062 6.71159 5.4227 6.70992 5.4252L6.70492 5.4327C6.70408 5.4352 6.70325 5.43812 6.70241 5.44146C6.70158 5.44396 6.70116 5.44688 6.70116 5.45021V6.61569L9.61735 4.93125C9.64737 4.91374 9.67988 4.90124 9.71239 4.89123C9.74616 4.88248 9.77992 4.87873 9.81368 4.87873C9.8487 4.87873 9.88246 4.88248 9.91623 4.89123C9.94874 4.90124 9.98 4.91374 10.01 4.93125L12.3997 6.31181C12.421 6.32432 12.4523 6.34182 12.4698 6.35308ZM6.15094 5.06255C6.15094 5.02879 6.15594 4.99502 6.1647 4.96126C6.17345 4.92875 6.18595 4.89623 6.20346 4.86622C6.22097 4.83746 6.24098 4.80995 6.26599 4.78494C6.28975 4.76118 6.31726 4.73992 6.34727 4.72366L8.73699 3.34435C8.7595 3.3306 8.79077 3.31309 8.80827 3.30433C8.48064 3.03047 8.08048 2.8554 7.65655 2.80163C7.23263 2.74661 6.80246 2.81413 6.41605 2.99546C6.02839 3.17678 5.70076 3.46565 5.47191 3.8258C5.24307 4.18719 5.12177 4.60487 5.12177 5.03254V7.82118C5.1226 7.82451 5.12344 7.82743 5.12427 7.82993C5.1251 7.83243 5.12635 7.83493 5.12802 7.83744C5.12969 7.83994 5.13177 7.84244 5.13427 7.84494C5.13594 7.84661 5.13844 7.84827 5.14178 7.84994L6.15094 8.43268V5.06255ZM6.69866 8.74781L7.99794 9.49811L9.29722 8.74781V7.24845L7.99919 6.49814L6.69991 7.24845L6.69866 8.74781Z" fill="white" />
  49. </svg>
  50. }
  51. /**
  52. * Icon component with numbers
  53. */
  54. const HandThumbIconWithCount: FC<{ count: number; iconType: 'up' | 'down' }> = ({ count, iconType }) => {
  55. const classname = iconType === 'up' ? 'text-primary-600 bg-primary-50' : 'text-red-600 bg-red-50'
  56. const Icon = iconType === 'up' ? HandThumbUpIcon : HandThumbDownIcon
  57. return <div className={`inline-flex items-center w-fit rounded-md p-1 text-xs ${classname} mr-1 last:mr-0`}>
  58. <Icon className={'h-3 w-3 mr-0.5 rounded-md'} />
  59. {count > 0 ? count : null}
  60. </div>
  61. }
  62. const PARAM_MAP = {
  63. temperature: 'Temperature',
  64. top_p: 'Top P',
  65. presence_penalty: 'Presence Penalty',
  66. max_tokens: 'Max Token',
  67. stop: 'Stop',
  68. frequency_penalty: 'Frequency Penalty',
  69. }
  70. // Format interface data for easy display
  71. const getFormattedChatList = (messages: ChatMessage[]) => {
  72. const newChatList: IChatItem[] = []
  73. messages.forEach((item: ChatMessage) => {
  74. newChatList.push({
  75. id: `question-${item.id}`,
  76. content: item.inputs.query || item.query, // text generation: item.inputs.query; chat: item.query
  77. isAnswer: false,
  78. })
  79. newChatList.push({
  80. id: item.id,
  81. content: item.answer,
  82. feedback: item.feedbacks.find(item => item.from_source === 'user'), // user feedback
  83. adminFeedback: item.feedbacks.find(item => item.from_source === 'admin'), // admin feedback
  84. feedbackDisabled: false,
  85. isAnswer: true,
  86. more: {
  87. time: dayjs.unix(item.created_at).format('hh:mm A'),
  88. tokens: item.answer_tokens + item.message_tokens,
  89. latency: item.provider_response_latency.toFixed(2),
  90. },
  91. annotation: item.annotation,
  92. })
  93. })
  94. return newChatList
  95. }
  96. // const displayedParams = CompletionParams.slice(0, -2)
  97. const validatedParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty']
  98. type IDetailPanel<T> = {
  99. detail: T
  100. onFeedback: FeedbackFunc
  101. onSubmitAnnotation: SubmitAnnotationFunc
  102. }
  103. function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionConversationFullDetailResponse>({ detail, onFeedback, onSubmitAnnotation }: IDetailPanel<T>) {
  104. const { onClose, appDetail } = useContext(DrawerContext)
  105. const { t } = useTranslation()
  106. const [items, setItems] = React.useState<IChatItem[]>([])
  107. const [hasMore, setHasMore] = useState(true)
  108. const fetchData = async () => {
  109. try {
  110. if (!hasMore)
  111. return
  112. const params: ChatMessagesRequest = {
  113. conversation_id: detail.id,
  114. limit: 4,
  115. }
  116. if (items?.[0]?.id)
  117. params.first_id = items?.[0]?.id.replace('question-', '')
  118. const messageRes = await fetchChatMessages({
  119. url: `/apps/${appDetail?.id}/chat-messages`,
  120. params,
  121. })
  122. const newItems = [...getFormattedChatList(messageRes.data), ...items]
  123. if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
  124. newItems.unshift({
  125. id: 'introduction',
  126. isAnswer: true,
  127. isOpeningStatement: true,
  128. content: detail?.model_config?.configs?.introduction ?? 'hello',
  129. feedbackDisabled: true,
  130. })
  131. }
  132. setItems(newItems)
  133. setHasMore(messageRes.has_more)
  134. }
  135. catch (err) {
  136. console.error(err)
  137. }
  138. }
  139. useEffect(() => {
  140. if (appDetail?.id && detail.id && appDetail?.mode === 'chat')
  141. fetchData()
  142. }, [appDetail?.id, detail.id])
  143. const isChatMode = appDetail?.mode === 'chat'
  144. const targetTone = TONE_LIST.find((item) => {
  145. let res = true
  146. validatedParams.forEach((param) => {
  147. res = item.config?.[param] === detail.model_config?.configs?.completion_params?.[param]
  148. })
  149. return res
  150. })?.name ?? 'custom'
  151. return (<div className='rounded-xl border-[0.5px] border-gray-200 h-full flex flex-col overflow-auto'>
  152. {/* Panel Header */}
  153. <div className='border-b border-gray-100 py-4 px-6 flex items-center justify-between'>
  154. <div className='flex-1'>
  155. <span className='text-gray-500 text-[10px]'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</span>
  156. <div className='text-gray-800 text-sm'>{isChatMode ? detail.id : dayjs.unix(detail.created_at).format(t('appLog.dateTimeFormat'))}</div>
  157. </div>
  158. <div className='mr-2 bg-gray-50 py-1.5 px-2.5 rounded-lg flex items-center text-[13px]'><OpenAIIcon className='mr-2' />{detail.model_config.model_id}</div>
  159. <Popover
  160. position='br'
  161. className='!w-[280px]'
  162. btnClassName='mr-4 !bg-gray-50 !py-1.5 !px-2.5 border-none font-normal'
  163. btnElement={<>
  164. <span className='text-[13px]'>{targetTone}</span>
  165. <InformationCircleIcon className='h-4 w-4 text-gray-800 ml-1.5' />
  166. </>}
  167. htmlContent={<div className='w-[280px]'>
  168. <div className='flex justify-between py-2 px-4 font-medium text-sm text-gray-700'>
  169. <span>Tone of responses</span>
  170. <div>{targetTone}</div>
  171. </div>
  172. {['temperature', 'top_p', 'presence_penalty', 'max_tokens'].map((param, index) => {
  173. return <div className='flex justify-between py-2 px-4 bg-gray-50' key={index}>
  174. <span className='text-xs text-gray-700'>{PARAM_MAP[param]}</span>
  175. <span className='text-gray-800 font-medium text-xs'>{detail?.model_config.model?.completion_params?.[param] || '-'}</span>
  176. </div>
  177. })}
  178. </div>}
  179. />
  180. <div className='w-6 h-6 rounded-lg flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
  181. <XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
  182. </div>
  183. </div>
  184. {/* Panel Body */}
  185. <div className='bg-gray-50 border border-gray-100 px-4 py-3 mx-6 my-4 rounded-lg'>
  186. <div className='text-gray-500 text-xs flex items-center'>
  187. <SparklesIcon className='h-3 w-3 mr-1' />{isChatMode ? t('appLog.detail.promptTemplateBeforeChat') : t('appLog.detail.promptTemplate')}
  188. </div>
  189. <div className='text-gray-700 font-medium text-sm mt-2'>{detail.model_config?.pre_prompt || emptyText}</div>
  190. </div>
  191. {!isChatMode
  192. ? <div className="px-2.5 py-4">
  193. <Chat
  194. chatList={getFormattedChatList([detail.message])}
  195. isHideSendInput={true}
  196. onFeedback={onFeedback}
  197. onSubmitAnnotation={onSubmitAnnotation}
  198. displayScene='console'
  199. />
  200. </div>
  201. : items.length < 8
  202. ? <div className="px-2.5 pt-4 mb-4">
  203. <Chat
  204. chatList={items}
  205. isHideSendInput={true}
  206. onFeedback={onFeedback}
  207. onSubmitAnnotation={onSubmitAnnotation}
  208. displayScene='console'
  209. />
  210. </div>
  211. : <div
  212. className="px-2.5 py-4"
  213. id="scrollableDiv"
  214. style={{
  215. height: 1000, // Specify a value
  216. overflow: 'auto',
  217. display: 'flex',
  218. flexDirection: 'column-reverse',
  219. }}>
  220. {/* Put the scroll bar always on the bottom */}
  221. <InfiniteScroll
  222. scrollableTarget="scrollableDiv"
  223. dataLength={items.length}
  224. next={fetchData}
  225. hasMore={hasMore}
  226. loader={<div className='text-center text-gray-400 text-xs'>{t('appLog.detail.loading')}...</div>}
  227. // endMessage={<div className='text-center'>Nothing more to show</div>}
  228. // below props only if you need pull down functionality
  229. refreshFunction={fetchData}
  230. pullDownToRefresh
  231. pullDownToRefreshThreshold={50}
  232. // pullDownToRefreshContent={
  233. // <div className='text-center'>Pull down to refresh</div>
  234. // }
  235. // releaseToRefreshContent={
  236. // <div className='text-center'>Release to refresh</div>
  237. // }
  238. // To put endMessage and loader to the top.
  239. style={{ display: 'flex', flexDirection: 'column-reverse' }}
  240. inverse={true}
  241. >
  242. <Chat
  243. chatList={items}
  244. isHideSendInput={true}
  245. onFeedback={onFeedback}
  246. onSubmitAnnotation={onSubmitAnnotation}
  247. displayScene='console'
  248. />
  249. </InfiniteScroll>
  250. </div>
  251. }
  252. </div>)
  253. }
  254. /**
  255. * Text App Conversation Detail Component
  256. */
  257. const CompletionConversationDetailComp: FC<{ appId?: string; conversationId?: string }> = ({ appId, conversationId }) => {
  258. // Text Generator App Session Details Including Message List
  259. const detailParams = ({ url: `/apps/${appId}/completion-conversations/${conversationId}` })
  260. const { data: conversationDetail, mutate: conversationDetailMutate } = useSWR(() => (appId && conversationId) ? detailParams : null, fetchCompletionConversationDetail)
  261. const { notify } = useContext(ToastContext)
  262. const { t } = useTranslation()
  263. const handleFeedback = async (mid: string, { rating }: Feedbacktype): Promise<boolean> => {
  264. try {
  265. await updateLogMessageFeedbacks({ url: `/apps/${appId}/feedbacks`, body: { message_id: mid, rating } })
  266. conversationDetailMutate()
  267. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  268. return true
  269. }
  270. catch (err) {
  271. notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
  272. return false
  273. }
  274. }
  275. const handleAnnotation = async (mid: string, value: string): Promise<boolean> => {
  276. try {
  277. await updateLogMessageAnnotations({ url: `/apps/${appId}/annotations`, body: { message_id: mid, content: value } })
  278. conversationDetailMutate()
  279. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  280. return true
  281. }
  282. catch (err) {
  283. notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
  284. return false
  285. }
  286. }
  287. if (!conversationDetail)
  288. return null
  289. return <DetailPanel<CompletionConversationFullDetailResponse>
  290. detail={conversationDetail}
  291. onFeedback={handleFeedback}
  292. onSubmitAnnotation={handleAnnotation}
  293. />
  294. }
  295. /**
  296. * Chat App Conversation Detail Component
  297. */
  298. const ChatConversationDetailComp: FC<{ appId?: string; conversationId?: string }> = ({ appId, conversationId }) => {
  299. const detailParams = { url: `/apps/${appId}/chat-conversations/${conversationId}` }
  300. const { data: conversationDetail } = useSWR(() => (appId && conversationId) ? detailParams : null, fetchChatConversationDetail)
  301. const { notify } = useContext(ToastContext)
  302. const { t } = useTranslation()
  303. const handleFeedback = async (mid: string, { rating }: Feedbacktype): Promise<boolean> => {
  304. try {
  305. await updateLogMessageFeedbacks({ url: `/apps/${appId}/feedbacks`, body: { message_id: mid, rating } })
  306. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  307. return true
  308. }
  309. catch (err) {
  310. notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
  311. return false
  312. }
  313. }
  314. const handleAnnotation = async (mid: string, value: string): Promise<boolean> => {
  315. try {
  316. await updateLogMessageAnnotations({ url: `/apps/${appId}/annotations`, body: { message_id: mid, content: value } })
  317. notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
  318. return true
  319. }
  320. catch (err) {
  321. notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
  322. return false
  323. }
  324. }
  325. if (!conversationDetail)
  326. return null
  327. return <DetailPanel<ChatConversationFullDetailResponse>
  328. detail={conversationDetail}
  329. onFeedback={handleFeedback}
  330. onSubmitAnnotation={handleAnnotation}
  331. />
  332. }
  333. /**
  334. * Conversation list component including basic information
  335. */
  336. const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh }) => {
  337. const { t } = useTranslation()
  338. const [showDrawer, setShowDrawer] = useState<boolean>(false) // Whether to display the chat details drawer
  339. const [currentConversation, setCurrentConversation] = useState<ChatConversationGeneralDetail | CompletionConversationGeneralDetail | undefined>() // Currently selected conversation
  340. const isChatMode = appDetail?.mode === 'chat' // Whether the app is a chat app
  341. // Annotated data needs to be highlighted
  342. const renderTdValue = (value: string | number | null, isEmptyStyle: boolean, isHighlight = false, annotation?: Annotation) => {
  343. return (
  344. <Tooltip
  345. htmlContent={
  346. <span className='text-xs text-gray-500 inline-flex items-center'>
  347. <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')}`}
  348. </span>
  349. }
  350. className={(isHighlight && !isChatMode) ? '' : '!hidden'}
  351. selector={`highlight-${randomString(16)}`}
  352. >
  353. <div className={classNames(isEmptyStyle ? 'text-gray-400' : 'text-gray-700', !isHighlight ? '' : 'bg-orange-100', 'text-sm overflow-hidden text-ellipsis whitespace-nowrap')}>
  354. {value || '-'}
  355. </div>
  356. </Tooltip>
  357. )
  358. }
  359. const onCloseDrawer = () => {
  360. onRefresh()
  361. setShowDrawer(false)
  362. setCurrentConversation(undefined)
  363. }
  364. if (!logs)
  365. return <Loading />
  366. return (
  367. <>
  368. <table className={`w-full border-collapse border-0 text-sm mt-3 ${s.logTable}`}>
  369. <thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-bold">
  370. <tr>
  371. <td className='w-[1.375rem]'></td>
  372. <td>{t('appLog.table.header.time')}</td>
  373. <td>{t('appLog.table.header.endUser')}</td>
  374. <td>{isChatMode ? t('appLog.table.header.summary') : t('appLog.table.header.input')}</td>
  375. <td>{isChatMode ? t('appLog.table.header.messageCount') : t('appLog.table.header.output')}</td>
  376. <td>{t('appLog.table.header.userRate')}</td>
  377. <td>{t('appLog.table.header.adminRate')}</td>
  378. </tr>
  379. </thead>
  380. <tbody className="text-gray-500">
  381. {logs.data.map((log) => {
  382. const endUser = log.from_end_user_session_id
  383. const leftValue = get(log, isChatMode ? 'summary' : 'message.inputs.query')
  384. const rightValue = get(log, isChatMode ? 'message_count' : 'message.answer')
  385. return <tr
  386. key={log.id}
  387. className={`border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer ${currentConversation?.id !== log.id ? '' : 'bg-gray-50'}`}
  388. onClick={() => {
  389. setShowDrawer(true)
  390. setCurrentConversation(log)
  391. }}>
  392. <td className='text-center align-middle'>{!log.read_at && <span className='inline-block bg-[#3F83F8] h-1.5 w-1.5 rounded'></span>}</td>
  393. <td className='w-[160px]'>{dayjs.unix(log.created_at).format(t('appLog.dateTimeFormat'))}</td>
  394. <td>{renderTdValue(endUser || defaultValue, !endUser)}</td>
  395. <td style={{ maxWidth: isChatMode ? 300 : 200 }}>
  396. {renderTdValue(leftValue || t('appLog.table.empty.noChat'), !leftValue, isChatMode && log.annotated)}
  397. </td>
  398. <td style={{ maxWidth: isChatMode ? 100 : 200 }}>
  399. {renderTdValue(rightValue === 0 ? 0 : (rightValue || t('appLog.table.empty.noOutput')), !rightValue, !isChatMode && !!log.annotation?.content, log.annotation)}
  400. </td>
  401. <td>
  402. {(!log.user_feedback_stats.like && !log.user_feedback_stats.dislike)
  403. ? renderTdValue(defaultValue, true)
  404. : <>
  405. {!!log.user_feedback_stats.like && <HandThumbIconWithCount iconType='up' count={log.user_feedback_stats.like} />}
  406. {!!log.user_feedback_stats.dislike && <HandThumbIconWithCount iconType='down' count={log.user_feedback_stats.dislike} />}
  407. </>
  408. }
  409. </td>
  410. <td>
  411. {(!log.admin_feedback_stats.like && !log.admin_feedback_stats.dislike)
  412. ? renderTdValue(defaultValue, true)
  413. : <>
  414. {!!log.admin_feedback_stats.like && <HandThumbIconWithCount iconType='up' count={log.admin_feedback_stats.like} />}
  415. {!!log.admin_feedback_stats.dislike && <HandThumbIconWithCount iconType='down' count={log.admin_feedback_stats.dislike} />}
  416. </>
  417. }
  418. </td>
  419. </tr>
  420. })}
  421. </tbody>
  422. </table>
  423. <Drawer
  424. isOpen={showDrawer}
  425. onClose={onCloseDrawer}
  426. mask={false}
  427. footer={null}
  428. panelClassname='mt-16 mr-2 mb-3 !p-0 !max-w-[640px] rounded-b-xl'
  429. >
  430. <DrawerContext.Provider value={{
  431. onClose: onCloseDrawer,
  432. appDetail,
  433. }}>
  434. {isChatMode
  435. ? <ChatConversationDetailComp appId={appDetail?.id} conversationId={currentConversation?.id} />
  436. : <CompletionConversationDetailComp appId={appDetail?.id} conversationId={currentConversation?.id} />
  437. }
  438. </DrawerContext.Provider>
  439. </Drawer>
  440. </>
  441. )
  442. }
  443. export default ConversationList