index.tsx 17 KB

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