index.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  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 AudioBtn from '@/app/components/base/audio-btn'
  16. import type { Feedbacktype } from '@/app/components/app/chat/type'
  17. import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
  18. import { Clipboard, File02 } from '@/app/components/base/icons/src/vender/line/files'
  19. import { Bookmark } from '@/app/components/base/icons/src/vender/line/general'
  20. import { Stars02 } from '@/app/components/base/icons/src/vender/line/weather'
  21. import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
  22. import { fetchTextGenerationMessge } from '@/service/debug'
  23. import AnnotationCtrlBtn from '@/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn'
  24. import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
  25. const MAX_DEPTH = 3
  26. export type IGenerationItemProps = {
  27. className?: string
  28. isError: boolean
  29. onRetry: () => void
  30. content: string
  31. messageId?: string | null
  32. conversationId?: string
  33. isLoading?: boolean
  34. isResponding?: boolean
  35. isInWebApp?: boolean
  36. moreLikeThis?: boolean
  37. depth?: number
  38. feedback?: Feedbacktype
  39. onFeedback?: (feedback: Feedbacktype) => void
  40. onSave?: (messageId: string) => void
  41. isMobile?: boolean
  42. isInstalledApp: boolean
  43. installedAppId?: string
  44. taskId?: string
  45. controlClearMoreLikeThis?: number
  46. supportFeedback?: boolean
  47. supportAnnotation?: boolean
  48. isShowTextToSpeech?: boolean
  49. appId?: string
  50. varList?: { label: string; value: string | number | object }[]
  51. innerClassName?: string
  52. contentClassName?: string
  53. footerClassName?: string
  54. }
  55. export const SimpleBtn = ({ className, isDisabled, onClick, children }: {
  56. className?: string
  57. isDisabled?: boolean
  58. onClick?: () => void
  59. children: React.ReactNode
  60. }) => (
  61. <div
  62. 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')}
  63. onClick={() => !isDisabled && onClick?.()}
  64. >
  65. {children}
  66. </div>
  67. )
  68. export const copyIcon = (
  69. <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
  70. <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" />
  71. </svg>
  72. )
  73. const GenerationItem: FC<IGenerationItemProps> = ({
  74. className,
  75. isError,
  76. onRetry,
  77. content,
  78. messageId,
  79. isLoading,
  80. isResponding,
  81. moreLikeThis,
  82. isInWebApp = false,
  83. feedback,
  84. onFeedback,
  85. onSave,
  86. depth = 1,
  87. isMobile,
  88. isInstalledApp,
  89. installedAppId,
  90. taskId,
  91. controlClearMoreLikeThis,
  92. supportFeedback,
  93. supportAnnotation,
  94. isShowTextToSpeech,
  95. appId,
  96. varList,
  97. innerClassName,
  98. contentClassName,
  99. }) => {
  100. const { t } = useTranslation()
  101. const params = useParams()
  102. const isTop = depth === 1
  103. const ref = useRef(null)
  104. const [completionRes, setCompletionRes] = useState('')
  105. const [childMessageId, setChildMessageId] = useState<string | null>(null)
  106. const hasChild = !!childMessageId
  107. const [childFeedback, setChildFeedback] = useState<Feedbacktype>({
  108. rating: null,
  109. })
  110. const [promptLog, setPromptLog] = useState<{ role: string; text: string }[]>([])
  111. const handleFeedback = async (childFeedback: Feedbacktype) => {
  112. await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, isInstalledApp, installedAppId)
  113. setChildFeedback(childFeedback)
  114. }
  115. const [isShowReplyModal, setIsShowReplyModal] = useState(false)
  116. const question = (varList && varList?.length > 0) ? varList?.map(({ label, value }) => `${label}:${value}`).join('&') : ''
  117. const [isQuerying, { setTrue: startQuerying, setFalse: stopQuerying }] = useBoolean(false)
  118. const childProps = {
  119. isInWebApp: true,
  120. content: completionRes,
  121. messageId: childMessageId,
  122. depth: depth + 1,
  123. moreLikeThis: true,
  124. onFeedback: handleFeedback,
  125. isLoading: isQuerying,
  126. feedback: childFeedback,
  127. onSave,
  128. isShowTextToSpeech,
  129. isMobile,
  130. isInstalledApp,
  131. installedAppId,
  132. controlClearMoreLikeThis,
  133. }
  134. const handleMoreLikeThis = async () => {
  135. if (isQuerying || !messageId) {
  136. Toast.notify({ type: 'warning', message: t('appDebug.errorMessage.waitForResponse') })
  137. return
  138. }
  139. startQuerying()
  140. const res: any = await fetchMoreLikeThis(messageId as string, isInstalledApp, installedAppId)
  141. setCompletionRes(res.answer)
  142. setChildFeedback({
  143. rating: null,
  144. })
  145. setChildMessageId(res.id)
  146. stopQuerying()
  147. }
  148. const mainStyle = (() => {
  149. const res: React.CSSProperties = !isTop
  150. ? {
  151. background: depth % 2 === 0 ? 'linear-gradient(90.07deg, #F9FAFB 0.05%, rgba(249, 250, 251, 0) 99.93%)' : '#fff',
  152. }
  153. : {}
  154. if (hasChild)
  155. res.boxShadow = '0px 1px 2px rgba(16, 24, 40, 0.05)'
  156. return res
  157. })()
  158. useEffect(() => {
  159. if (controlClearMoreLikeThis) {
  160. setChildMessageId(null)
  161. setCompletionRes('')
  162. }
  163. }, [controlClearMoreLikeThis])
  164. // regeneration clear child
  165. useEffect(() => {
  166. if (isLoading)
  167. setChildMessageId(null)
  168. }, [isLoading])
  169. const handleOpenLogModal = async (setModal: Dispatch<SetStateAction<boolean>>) => {
  170. const data = await fetchTextGenerationMessge({
  171. appId: params.appId as string,
  172. messageId: messageId!,
  173. })
  174. setPromptLog(data.message as any || [])
  175. setModal(true)
  176. }
  177. const ratingContent = (
  178. <>
  179. {!isError && messageId && !feedback?.rating && (
  180. <SimpleBtn className="!px-0">
  181. <>
  182. <div
  183. onClick={() => {
  184. onFeedback?.({
  185. rating: 'like',
  186. })
  187. }}
  188. className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
  189. <HandThumbUpIcon width={16} height={16} />
  190. </div>
  191. <div
  192. onClick={() => {
  193. onFeedback?.({
  194. rating: 'dislike',
  195. })
  196. }}
  197. className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
  198. <HandThumbDownIcon width={16} height={16} />
  199. </div>
  200. </>
  201. </SimpleBtn>
  202. )}
  203. {!isError && messageId && feedback?.rating === 'like' && (
  204. <div
  205. onClick={() => {
  206. onFeedback?.({
  207. rating: null,
  208. })
  209. }}
  210. 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'>
  211. <HandThumbUpIcon width={16} height={16} />
  212. </div>
  213. )}
  214. {!isError && messageId && feedback?.rating === 'dislike' && (
  215. <div
  216. onClick={() => {
  217. onFeedback?.({
  218. rating: null,
  219. })
  220. }}
  221. 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'>
  222. <HandThumbDownIcon width={16} height={16} />
  223. </div>
  224. )}
  225. </>
  226. )
  227. return (
  228. <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')}
  229. style={isTop
  230. ? {
  231. boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
  232. }
  233. : {}}
  234. >
  235. {isLoading
  236. ? (
  237. <div className='flex items-center h-10'><Loading type='area' /></div>
  238. )
  239. : (
  240. <div
  241. className={cn(!isTop && 'rounded-br-xl border-l-2 border-primary-400', 'p-4', innerClassName)}
  242. style={mainStyle}
  243. >
  244. {(isTop && taskId) && (
  245. <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'>
  246. <HashtagIcon className='w-3 h-3 text-gray-400 fill-current mr-1 stroke-current stroke-1' />
  247. {taskId}
  248. </div>)
  249. }
  250. <div className={`flex ${contentClassName}`}>
  251. <div className='grow w-0'>
  252. {isError
  253. ? <div className='text-gray-400 text-sm'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
  254. : (
  255. <Markdown content={content} />
  256. )}
  257. </div>
  258. </div>
  259. <div className='flex items-center justify-between mt-3'>
  260. <div className='flex items-center'>
  261. {
  262. !isInWebApp && !isInstalledApp && !isResponding && (
  263. <PromptLog
  264. log={promptLog}
  265. containerRef={ref}
  266. >
  267. {
  268. showModal => (
  269. <SimpleBtn
  270. isDisabled={isError || !messageId}
  271. className={cn(isMobile && '!px-1.5', 'space-x-1 mr-1')}
  272. onClick={() => handleOpenLogModal(showModal)}>
  273. <File02 className='w-3.5 h-3.5' />
  274. {!isMobile && <div>{t('common.operation.log')}</div>}
  275. </SimpleBtn>
  276. )
  277. }
  278. </PromptLog>
  279. )
  280. }
  281. <SimpleBtn
  282. isDisabled={isError || !messageId}
  283. className={cn(isMobile && '!px-1.5', 'space-x-1')}
  284. onClick={() => {
  285. copy(content)
  286. Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
  287. }}>
  288. <Clipboard className='w-3.5 h-3.5' />
  289. {!isMobile && <div>{t('common.operation.copy')}</div>}
  290. </SimpleBtn>
  291. {isInWebApp && (
  292. <>
  293. <SimpleBtn
  294. isDisabled={isError || !messageId}
  295. className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
  296. onClick={() => { onSave?.(messageId as string) }}
  297. >
  298. <Bookmark className='w-3.5 h-3.5' />
  299. {!isMobile && <div>{t('common.operation.save')}</div>}
  300. </SimpleBtn>
  301. {(moreLikeThis && depth < MAX_DEPTH) && (
  302. <SimpleBtn
  303. isDisabled={isError || !messageId}
  304. className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
  305. onClick={handleMoreLikeThis}
  306. >
  307. <Stars02 className='w-3.5 h-3.5' />
  308. {!isMobile && <div>{t('appDebug.feature.moreLikeThis.title')}</div>}
  309. </SimpleBtn>)}
  310. {isError && <SimpleBtn
  311. onClick={onRetry}
  312. className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
  313. >
  314. <RefreshCcw01 className='w-3.5 h-3.5' />
  315. {!isMobile && <div>{t('share.generation.batchFailed.retry')}</div>}
  316. </SimpleBtn>}
  317. {!isError && messageId && <div className="mx-3 w-[1px] h-[14px] bg-gray-200"></div>}
  318. {ratingContent}
  319. </>
  320. )}
  321. {supportAnnotation && (
  322. <>
  323. <div className='ml-2 mr-1 h-[14px] w-[1px] bg-gray-200'></div>
  324. <AnnotationCtrlBtn
  325. appId={appId!}
  326. messageId={messageId!}
  327. className='ml-1'
  328. query={question}
  329. answer={content}
  330. // not support cache. So can not be cached
  331. cached={false}
  332. onAdded={() => {
  333. }}
  334. onEdit={() => setIsShowReplyModal(true)}
  335. onRemoved={() => { }}
  336. />
  337. </>
  338. )}
  339. <EditReplyModal
  340. appId={appId!}
  341. messageId={messageId!}
  342. isShow={isShowReplyModal}
  343. onHide={() => setIsShowReplyModal(false)}
  344. query={question}
  345. answer={content}
  346. onAdded={() => { }}
  347. onEdited={() => { }}
  348. createdAt={0}
  349. onRemove={() => { }}
  350. onlyEditResponse
  351. />
  352. {supportFeedback && (
  353. <div className='ml-1'>
  354. {ratingContent}
  355. </div>
  356. )}
  357. {isShowTextToSpeech && (
  358. <>
  359. <div className='ml-2 mr-2 h-[14px] w-[1px] bg-gray-200'></div>
  360. <AudioBtn
  361. value={content}
  362. className={'mr-1'}
  363. />
  364. </>
  365. )}
  366. </div>
  367. <div className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
  368. </div>
  369. </div>
  370. )}
  371. {((childMessageId || isQuerying) && depth < 3) && (
  372. <div className='pl-4'>
  373. <GenerationItem {...childProps as any} />
  374. </div>
  375. )}
  376. </div>
  377. )
  378. }
  379. export default React.memo(GenerationItem)