index.tsx 17 KB

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