index.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. 'use client'
  2. import type { Dispatch, FC, SetStateAction } from 'react'
  3. import React, { useEffect, useRef, useState } from 'react'
  4. import { useTranslation } from 'react-i18next'
  5. import cn from 'classnames'
  6. import copy from 'copy-to-clipboard'
  7. import { useParams } from 'next/navigation'
  8. import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
  9. import { useBoolean } from 'ahooks'
  10. import { HashtagIcon } from '@heroicons/react/24/solid'
  11. import PromptLog from '@/app/components/app/chat/log'
  12. import { Markdown } from '@/app/components/base/markdown'
  13. import Loading from '@/app/components/base/loading'
  14. import Toast from '@/app/components/base/toast'
  15. import type { Feedbacktype } from '@/app/components/app/chat/type'
  16. import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
  17. import { Clipboard, File02 } from '@/app/components/base/icons/src/vender/line/files'
  18. import { Bookmark } from '@/app/components/base/icons/src/vender/line/general'
  19. import { Stars02 } from '@/app/components/base/icons/src/vender/line/weather'
  20. import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
  21. import { fetchTextGenerationMessge } from '@/service/debug'
  22. import AnnotationCtrlBtn from '@/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn'
  23. import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
  24. const MAX_DEPTH = 3
  25. export type IGenerationItemProps = {
  26. className?: string
  27. isError: boolean
  28. onRetry: () => void
  29. content: string
  30. messageId?: string | null
  31. conversationId?: string
  32. isLoading?: boolean
  33. isResponsing?: boolean
  34. isInWebApp?: boolean
  35. moreLikeThis?: boolean
  36. depth?: number
  37. feedback?: Feedbacktype
  38. onFeedback?: (feedback: Feedbacktype) => void
  39. onSave?: (messageId: string) => void
  40. isMobile?: boolean
  41. isInstalledApp: boolean
  42. installedAppId?: string
  43. taskId?: string
  44. controlClearMoreLikeThis?: number
  45. supportFeedback?: boolean
  46. supportAnnotation?: boolean
  47. appId?: string
  48. varList?: { label: string; value: string | number | object }[]
  49. }
  50. export const SimpleBtn = ({ className, isDisabled, onClick, children }: {
  51. className?: string
  52. isDisabled?: boolean
  53. onClick?: () => void
  54. children: React.ReactNode
  55. }) => (
  56. <div
  57. className={cn(className, isDisabled ? 'border-gray-100 text-gray-300' : 'border-gray-200 text-gray-700 cursor-pointer hover:border-gray-300 hover:shadow-sm', 'flex items-center h-7 px-3 rounded-md border text-xs font-medium')}
  58. onClick={() => !isDisabled && onClick?.()}
  59. >
  60. {children}
  61. </div>
  62. )
  63. export const copyIcon = (
  64. <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
  65. <path d="M9.3335 2.33341C9.87598 2.33341 10.1472 2.33341 10.3698 2.39304C10.9737 2.55486 11.4454 3.02657 11.6072 3.63048C11.6668 3.85302 11.6668 4.12426 11.6668 4.66675V10.0334C11.6668 11.0135 11.6668 11.5036 11.4761 11.8779C11.3083 12.2072 11.0406 12.4749 10.7113 12.6427C10.337 12.8334 9.84692 12.8334 8.86683 12.8334H5.1335C4.1534 12.8334 3.66336 12.8334 3.28901 12.6427C2.95973 12.4749 2.69201 12.2072 2.52423 11.8779C2.3335 11.5036 2.3335 11.0135 2.3335 10.0334V4.66675C2.3335 4.12426 2.3335 3.85302 2.39313 3.63048C2.55494 3.02657 3.02665 2.55486 3.63056 2.39304C3.8531 2.33341 4.12435 2.33341 4.66683 2.33341M5.60016 3.50008H8.40016C8.72686 3.50008 8.89021 3.50008 9.01499 3.4365C9.12475 3.38058 9.21399 3.29134 9.26992 3.18158C9.3335 3.05679 9.3335 2.89345 9.3335 2.56675V2.10008C9.3335 1.77338 9.3335 1.61004 9.26992 1.48525C9.21399 1.37549 9.12475 1.28625 9.01499 1.23033C8.89021 1.16675 8.72686 1.16675 8.40016 1.16675H5.60016C5.27347 1.16675 5.11012 1.16675 4.98534 1.23033C4.87557 1.28625 4.78634 1.37549 4.73041 1.48525C4.66683 1.61004 4.66683 1.77338 4.66683 2.10008V2.56675C4.66683 2.89345 4.66683 3.05679 4.73041 3.18158C4.78634 3.29134 4.87557 3.38058 4.98534 3.4365C5.11012 3.50008 5.27347 3.50008 5.60016 3.50008Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
  66. </svg>
  67. )
  68. const GenerationItem: FC<IGenerationItemProps> = ({
  69. className,
  70. isError,
  71. onRetry,
  72. content,
  73. messageId,
  74. isLoading,
  75. isResponsing,
  76. moreLikeThis,
  77. isInWebApp = false,
  78. feedback,
  79. onFeedback,
  80. onSave,
  81. depth = 1,
  82. isMobile,
  83. isInstalledApp,
  84. installedAppId,
  85. taskId,
  86. controlClearMoreLikeThis,
  87. supportFeedback,
  88. supportAnnotation,
  89. appId,
  90. varList,
  91. }) => {
  92. const { t } = useTranslation()
  93. const params = useParams()
  94. const isTop = depth === 1
  95. const ref = useRef(null)
  96. const [completionRes, setCompletionRes] = useState('')
  97. const [childMessageId, setChildMessageId] = useState<string | null>(null)
  98. const hasChild = !!childMessageId
  99. const [childFeedback, setChildFeedback] = useState<Feedbacktype>({
  100. rating: null,
  101. })
  102. const [promptLog, setPromptLog] = useState<{ role: string; text: string }[]>([])
  103. const handleFeedback = async (childFeedback: Feedbacktype) => {
  104. await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, isInstalledApp, installedAppId)
  105. setChildFeedback(childFeedback)
  106. }
  107. const [isShowReplyModal, setIsShowReplyModal] = useState(false)
  108. const question = (varList && varList?.length > 0) ? varList?.map(({ label, value }) => `${label}:${value}`).join('&') : ''
  109. const [isQuerying, { setTrue: startQuerying, setFalse: stopQuerying }] = useBoolean(false)
  110. const childProps = {
  111. isInWebApp: true,
  112. content: completionRes,
  113. messageId: childMessageId,
  114. depth: depth + 1,
  115. moreLikeThis: true,
  116. onFeedback: handleFeedback,
  117. isLoading: isQuerying,
  118. feedback: childFeedback,
  119. onSave,
  120. isMobile,
  121. isInstalledApp,
  122. installedAppId,
  123. controlClearMoreLikeThis,
  124. }
  125. const handleMoreLikeThis = async () => {
  126. if (isQuerying || !messageId) {
  127. Toast.notify({ type: 'warning', message: t('appDebug.errorMessage.waitForResponse') })
  128. return
  129. }
  130. startQuerying()
  131. const res: any = await fetchMoreLikeThis(messageId as string, isInstalledApp, installedAppId)
  132. setCompletionRes(res.answer)
  133. setChildFeedback({
  134. rating: null,
  135. })
  136. setChildMessageId(res.id)
  137. stopQuerying()
  138. }
  139. const mainStyle = (() => {
  140. const res: React.CSSProperties = !isTop
  141. ? {
  142. background: depth % 2 === 0 ? 'linear-gradient(90.07deg, #F9FAFB 0.05%, rgba(249, 250, 251, 0) 99.93%)' : '#fff',
  143. }
  144. : {}
  145. if (hasChild)
  146. res.boxShadow = '0px 1px 2px rgba(16, 24, 40, 0.05)'
  147. return res
  148. })()
  149. useEffect(() => {
  150. if (controlClearMoreLikeThis) {
  151. setChildMessageId(null)
  152. setCompletionRes('')
  153. }
  154. }, [controlClearMoreLikeThis])
  155. // regeneration clear child
  156. useEffect(() => {
  157. if (isLoading)
  158. setChildMessageId(null)
  159. }, [isLoading])
  160. const handleOpenLogModal = async (setModal: Dispatch<SetStateAction<boolean>>) => {
  161. const data = await fetchTextGenerationMessge({
  162. appId: params.appId,
  163. messageId: messageId!,
  164. })
  165. setPromptLog(data.message as any || [])
  166. setModal(true)
  167. }
  168. const ratingContent = (
  169. <>
  170. {!isError && messageId && !feedback?.rating && (
  171. <SimpleBtn className="!px-0">
  172. <>
  173. <div
  174. onClick={() => {
  175. onFeedback?.({
  176. rating: 'like',
  177. })
  178. }}
  179. className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
  180. <HandThumbUpIcon width={16} height={16} />
  181. </div>
  182. <div
  183. onClick={() => {
  184. onFeedback?.({
  185. rating: 'dislike',
  186. })
  187. }}
  188. className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
  189. <HandThumbDownIcon width={16} height={16} />
  190. </div>
  191. </>
  192. </SimpleBtn>
  193. )}
  194. {!isError && messageId && feedback?.rating === 'like' && (
  195. <div
  196. onClick={() => {
  197. onFeedback?.({
  198. rating: null,
  199. })
  200. }}
  201. className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-primary-600 border border-primary-200 bg-primary-100 hover:border-primary-300 hover:bg-primary-200'>
  202. <HandThumbUpIcon width={16} height={16} />
  203. </div>
  204. )}
  205. {!isError && messageId && feedback?.rating === 'dislike' && (
  206. <div
  207. onClick={() => {
  208. onFeedback?.({
  209. rating: null,
  210. })
  211. }}
  212. className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer !text-red-600 border border-red-200 bg-red-100 hover:border-red-300 hover:bg-red-200'>
  213. <HandThumbDownIcon width={16} height={16} />
  214. </div>
  215. )}
  216. </>
  217. )
  218. return (
  219. <div ref={ref} className={cn(className, isTop ? `rounded-xl border ${!isError ? 'border-gray-200 bg-white' : 'border-[#FECDCA] bg-[#FEF3F2]'} ` : 'rounded-br-xl !mt-0')}
  220. style={isTop
  221. ? {
  222. boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
  223. }
  224. : {}}
  225. >
  226. {isLoading
  227. ? (
  228. <div className='flex items-center h-10'><Loading type='area' /></div>
  229. )
  230. : (
  231. <div
  232. className={cn(!isTop && 'rounded-br-xl border-l-2 border-primary-400', 'p-4')}
  233. style={mainStyle}
  234. >
  235. {(isTop && taskId) && (
  236. <div className='mb-2 text-gray-500 border border-gray-200 box-border flex items-center rounded-md italic text-[11px] pl-1 pr-1.5 font-medium w-fit group-hover:opacity-100'>
  237. <HashtagIcon className='w-3 h-3 text-gray-400 fill-current mr-1 stroke-current stroke-1' />
  238. {taskId}
  239. </div>)
  240. }
  241. <div className='flex'>
  242. <div className='grow w-0'>
  243. {isError
  244. ? <div className='text-gray-400 text-sm'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
  245. : (
  246. <Markdown content={content} />
  247. )}
  248. </div>
  249. </div>
  250. <div className='flex items-center justify-between mt-3'>
  251. <div className='flex items-center'>
  252. {
  253. !isInWebApp && !isInstalledApp && !isResponsing && (
  254. <PromptLog
  255. log={promptLog}
  256. containerRef={ref}
  257. >
  258. {
  259. showModal => (
  260. <SimpleBtn
  261. isDisabled={isError || !messageId}
  262. className={cn(isMobile && '!px-1.5', 'space-x-1 mr-1')}
  263. onClick={() => handleOpenLogModal(showModal)}>
  264. <File02 className='w-3.5 h-3.5' />
  265. {!isMobile && <div>{t('common.operation.log')}</div>}
  266. </SimpleBtn>
  267. )
  268. }
  269. </PromptLog>
  270. )
  271. }
  272. <SimpleBtn
  273. isDisabled={isError || !messageId}
  274. className={cn(isMobile && '!px-1.5', 'space-x-1')}
  275. onClick={() => {
  276. copy(content)
  277. Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
  278. }}>
  279. <Clipboard className='w-3.5 h-3.5' />
  280. {!isMobile && <div>{t('common.operation.copy')}</div>}
  281. </SimpleBtn>
  282. {isInWebApp && (
  283. <>
  284. <SimpleBtn
  285. isDisabled={isError || !messageId}
  286. className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
  287. onClick={() => { onSave?.(messageId as string) }}
  288. >
  289. <Bookmark className='w-3.5 h-3.5' />
  290. {!isMobile && <div>{t('common.operation.save')}</div>}
  291. </SimpleBtn>
  292. {(moreLikeThis && depth < MAX_DEPTH) && (
  293. <SimpleBtn
  294. isDisabled={isError || !messageId}
  295. className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
  296. onClick={handleMoreLikeThis}
  297. >
  298. <Stars02 className='w-3.5 h-3.5' />
  299. {!isMobile && <div>{t('appDebug.feature.moreLikeThis.title')}</div>}
  300. </SimpleBtn>)}
  301. {isError && <SimpleBtn
  302. onClick={onRetry}
  303. className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
  304. >
  305. <RefreshCcw01 className='w-3.5 h-3.5' />
  306. {!isMobile && <div>{t('share.generation.batchFailed.retry')}</div>}
  307. </SimpleBtn>}
  308. {!isError && messageId && <div className="mx-3 w-[1px] h-[14px] bg-gray-200"></div>}
  309. {ratingContent}
  310. </>
  311. )}
  312. {supportAnnotation && (
  313. <>
  314. <div className='ml-2 mr-1 h-[14px] w-[1px] bg-gray-200'></div>
  315. <AnnotationCtrlBtn
  316. appId={appId!}
  317. messageId={messageId!}
  318. className='ml-1'
  319. query={question}
  320. answer={content}
  321. // not support cache. So can not be cached
  322. cached={false}
  323. onAdded={() => {
  324. }}
  325. onEdit={() => setIsShowReplyModal(true)}
  326. onRemoved={() => { }}
  327. />
  328. </>
  329. )}
  330. <EditReplyModal
  331. appId={appId!}
  332. messageId={messageId!}
  333. isShow={isShowReplyModal}
  334. onHide={() => setIsShowReplyModal(false)}
  335. query={question}
  336. answer={content}
  337. onAdded={() => { }}
  338. onEdited={() => { }}
  339. createdAt={0}
  340. onRemove={() => { }}
  341. onlyEditResponse
  342. />
  343. {supportFeedback && (
  344. <div className='ml-1'>
  345. {ratingContent}
  346. </div>
  347. )
  348. }
  349. </div>
  350. <div className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
  351. </div>
  352. </div>
  353. )}
  354. {((childMessageId || isQuerying) && depth < 3) && (
  355. <div className='pl-4'>
  356. <GenerationItem {...childProps as any} />
  357. </div>
  358. )}
  359. </div>
  360. )
  361. }
  362. export default React.memo(GenerationItem)