wwf
3 天以前 a430284aa21e3ae1f0d5654e55b2ad2852519cc2
app/components/app/text-generate/item/index.tsx
@@ -1,36 +1,35 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
  RiBookmark3Line,
  RiClipboardLine,
  RiFileList3Line,
  RiPlayList2Line,
  RiReplay15Line,
  RiSparklingFill,
  RiSparklingLine,
  RiThumbDownLine,
  RiThumbUpLine,
} from '@remixicon/react'
import copy from 'copy-to-clipboard'
import { useParams } from 'next/navigation'
import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
import { useBoolean } from 'ahooks'
import { HashtagIcon } from '@heroicons/react/24/solid'
import ResultTab from './result-tab'
import cn from '@/utils/classnames'
import { Markdown } from '@/app/components/base/markdown'
import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast'
import AudioBtn from '@/app/components/base/audio-btn'
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
import { File02 } from '@/app/components/base/icons/src/vender/line/files'
import { Bookmark } from '@/app/components/base/icons/src/vender/line/general'
import { Stars02 } from '@/app/components/base/icons/src/vender/line/weather'
import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
import AnnotationCtrlBtn from '@/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-btn'
import { fetchTextGenerationMessage } from '@/service/debug'
import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
import { useStore as useAppStore } from '@/app/components/app/store'
import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process'
import type { WorkflowProcess } from '@/app/components/base/chat/types'
import type { SiteInfo } from '@/models/share'
import { useChatContext } from '@/app/components/base/chat/chat/context'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import NewAudioButton from '@/app/components/base/new-audio-button'
import cn from '@/utils/classnames'
const MAX_DEPTH = 3
@@ -57,11 +56,30 @@
  taskId?: string
  controlClearMoreLikeThis?: number
  supportFeedback?: boolean
  supportAnnotation?: boolean
  isShowTextToSpeech?: boolean
  appId?: string
  varList?: { label: string; value: string | number | object }[]
  innerClassName?: string
  contentClassName?: string
  footerClassName?: string
  hideProcessDetail?: boolean
  siteInfo: SiteInfo | null
  inSidePanel?: boolean
}
export const SimpleBtn = ({ className, isDisabled, onClick, children }: {
  className?: string
  isDisabled?: boolean
  onClick?: () => void
  children: React.ReactNode
}) => (
  <div
    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)}
    onClick={() => !isDisabled && onClick?.()}
  >
    {children}
  </div>
)
export const copyIcon = (
  <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -91,16 +109,22 @@
  taskId,
  controlClearMoreLikeThis,
  supportFeedback,
  supportAnnotation,
  isShowTextToSpeech,
  appId,
  varList,
  innerClassName,
  contentClassName,
  hideProcessDetail,
  siteInfo,
  inSidePanel,
}) => {
  const { t } = useTranslation()
  const params = useParams()
  const isTop = depth === 1
  const ref = useRef(null)
  const [completionRes, setCompletionRes] = useState('')
  const [childMessageId, setChildMessageId] = useState<string | null>(null)
  const hasChild = !!childMessageId
  const [childFeedback, setChildFeedback] = useState<FeedbackType>({
    rating: null,
  })
@@ -116,6 +140,8 @@
    setChildFeedback(childFeedback)
  }
  const [isShowReplyModal, setIsShowReplyModal] = useState(false)
  const question = (varList && varList?.length > 0) ? varList?.map(({ label, value }) => `${label}:${value}`).join('&') : ''
  const [isQuerying, { setTrue: startQuerying, setFalse: stopQuerying }] = useBoolean(false)
  const childProps = {
@@ -135,7 +161,6 @@
    controlClearMoreLikeThis,
    isWorkflow,
    siteInfo,
    taskId,
  }
  const handleMoreLikeThis = async () => {
@@ -152,6 +177,19 @@
    setChildMessageId(res.id)
    stopQuerying()
  }
  const mainStyle = (() => {
    const res: React.CSSProperties = !isTop
      ? {
        background: depth % 2 === 0 ? 'linear-gradient(90.07deg, #F9FAFB 0.05%, rgba(249, 250, 251, 0) 99.93%)' : '#fff',
      }
      : {}
    if (hasChild)
      res.boxShadow = '0px 1px 2px rgba(16, 24, 40, 0.05)'
    return res
  })()
  useEffect(() => {
    if (controlClearMoreLikeThis) {
@@ -190,125 +228,123 @@
    setShowPromptLogModal(true)
  }
  const ratingContent = (
    <>
      {!isWorkflow && !isError && messageId && !feedback?.rating && (
        <SimpleBtn className="!px-0">
          <>
            <div
              onClick={() => {
                onFeedback?.({
                  rating: 'like',
                })
              }}
              className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
              <HandThumbUpIcon width={16} height={16} />
            </div>
            <div
              onClick={() => {
                onFeedback?.({
                  rating: 'dislike',
                })
              }}
              className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
              <HandThumbDownIcon width={16} height={16} />
            </div>
          </>
        </SimpleBtn>
      )}
      {!isWorkflow && !isError && messageId && feedback?.rating === 'like' && (
        <div
          onClick={() => {
            onFeedback?.({
              rating: null,
            })
          }}
          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'>
          <HandThumbUpIcon width={16} height={16} />
        </div>
      )}
      {!isWorkflow && !isError && messageId && feedback?.rating === 'dislike' && (
        <div
          onClick={() => {
            onFeedback?.({
              rating: null,
            })
          }}
          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'>
          <HandThumbDownIcon width={16} height={16} />
        </div>
      )}
    </>
  )
  const [currentTab, setCurrentTab] = useState<string>('DETAIL')
  const showResultTabs = !!workflowProcessData?.resultText || !!workflowProcessData?.files?.length
  const switchTab = async (tab: string) => {
    setCurrentTab(tab)
  }
  useEffect(() => {
    if (workflowProcessData?.resultText || !!workflowProcessData?.files?.length)
      switchTab('RESULT')
    else
      switchTab('DETAIL')
  }, [workflowProcessData?.files?.length, workflowProcessData?.resultText])
  return (
    <>
      <div className={cn('relative', !isTop && 'mt-3', className)}>
        {isLoading && (
          <div className={cn('flex h-10 items-center', !inSidePanel && 'rounded-2xl border-t border-divider-subtle bg-chat-bubble-bg')}><Loading type='area' /></div>
        )}
        {!isLoading && (
          <>
            {/* result content */}
            <div className={cn(
              'relative',
              !inSidePanel && 'rounded-2xl border-t border-divider-subtle bg-chat-bubble-bg',
            )}>
              {workflowProcessData && (
                <>
                  <div className={cn(
                    'p-3',
                    showResultTabs && 'border-b border-divider-subtle',
                  )}>
                    {taskId && (
                      <div className={cn('system-2xs-medium-uppercase mb-2 flex items-center text-text-accent-secondary', isError && 'text-text-destructive')}>
                        <RiPlayList2Line className='mr-1 h-3 w-3' />
                        <span>{t('share.generation.execution')}</span>
                        <span className='px-1'>·</span>
                        <span>{taskId}</span>
                      </div>
                    )}
                    {siteInfo && workflowProcessData && (
                      <WorkflowProcessItem
                        data={workflowProcessData}
                        expand={workflowProcessData.expand}
                        hideProcessDetail={hideProcessDetail}
                        hideInfo={hideProcessDetail}
                        readonly={!siteInfo.show_workflow_steps}
                      />
                    )}
                    {showResultTabs && (
                      <div className='flex items-center space-x-6 px-1'>
                        <div
                          className={cn(
                            'system-sm-semibold-uppercase cursor-pointer border-b-2 border-transparent py-3 text-text-tertiary',
                            currentTab === 'RESULT' && 'border-util-colors-blue-brand-blue-brand-600 text-text-primary',
                          )}
                          onClick={() => switchTab('RESULT')}
                        >{t('runLog.result')}</div>
                        <div
                          className={cn(
                            'system-sm-semibold-uppercase cursor-pointer border-b-2 border-transparent py-3 text-text-tertiary',
                            currentTab === 'DETAIL' && 'border-util-colors-blue-brand-blue-brand-600 text-text-primary',
                          )}
                          onClick={() => switchTab('DETAIL')}
                        >{t('runLog.detail')}</div>
                      </div>
                    )}
                  </div>
                  {!isError && (
                    <ResultTab data={workflowProcessData} content={content} currentTab={currentTab} />
                  )}
                </>
              )}
              {!workflowProcessData && taskId && (
                <div className={cn('system-2xs-medium-uppercase sticky left-0 top-0 flex w-full items-center rounded-t-2xl bg-components-actionbar-bg p-4 pb-3 text-text-accent-secondary', isError && 'text-text-destructive')}>
                  <RiPlayList2Line className='mr-1 h-3 w-3' />
                  <span>{t('share.generation.execution')}</span>
                  <span className='px-1'>·</span>
                  <span>{`${taskId}${depth > 1 ? `-${depth - 1}` : ''}`}</span>
                </div>
              )}
              {isError && (
                <div className='body-lg-regular p-4 pt-0 text-text-quaternary'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
              )}
              {!workflowProcessData && !isError && (typeof content === 'string') && (
                <div className={cn('p-4', taskId && 'pt-0')}>
                  <Markdown content={content} />
                </div>
              )}
            </div>
            {/* meta data */}
            <div className={cn(
              'system-xs-regular relative mt-1 h-4 px-4 text-text-quaternary',
              isMobile && ((childMessageId || isQuerying) && depth < 3) && 'pl-10',
            )}>
              {!isWorkflow && <span>{content?.length} {t('common.unit.char')}</span>}
              {/* action buttons */}
              <div className='absolute bottom-1 right-2 flex items-center'>
                {!isInWebApp && !isInstalledApp && !isResponding && (
                  <div className='ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm'>
                    <ActionButton disabled={isError || !messageId} onClick={handleOpenLogModal}>
                      <RiFileList3Line className='h-4 w-4' />
                      {/* <div>{t('common.operation.log')}</div> */}
                    </ActionButton>
                  </div>
    <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)}
      style={isTop
        ? {
          boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
        }
        : {}}
    >
      {isLoading
        ? (
          <div className='flex items-center h-10'><Loading type='area' /></div>
        )
        : (
          <div
            className={cn(!isTop && 'rounded-br-xl border-l-2 border-primary-400', 'p-4', innerClassName)}
            style={mainStyle}
          >
            {(isTop && taskId) && (
              <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'>
                <HashtagIcon className='w-3 h-3 text-gray-400 fill-current mr-1 stroke-current stroke-1' />
                {taskId}
              </div>)
            }
            <div className={`flex ${contentClassName}`}>
              <div className='grow w-0'>
                {siteInfo && workflowProcessData && (
                  <WorkflowProcessItem
                    data={workflowProcessData}
                    expand={workflowProcessData.expand}
                    hideProcessDetail={hideProcessDetail}
                    hideInfo={hideProcessDetail}
                    readonly={!siteInfo.show_workflow_steps}
                  />
                )}
                <div className='ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm'>
                  {moreLikeThis && (
                    <ActionButton state={depth === MAX_DEPTH ? ActionButtonState.Disabled : ActionButtonState.Default} disabled={depth === MAX_DEPTH} onClick={handleMoreLikeThis}>
                      <RiSparklingLine className='h-4 w-4' />
                    </ActionButton>
                  )}
                  {isShowTextToSpeech && (
                    <NewAudioButton
                      id={messageId!}
                      voice={config?.text_to_speech?.voice}
                    />
                  )}
                  {((currentTab === 'RESULT' && workflowProcessData?.resultText) || !isWorkflow) && (
                    <ActionButton disabled={isError || !messageId} onClick={() => {
                {workflowProcessData && !isError && (
                  <ResultTab data={workflowProcessData} content={content} currentTab={currentTab} onCurrentTabChange={setCurrentTab} />
                )}
                {isError && (
                  <div className='text-gray-400 text-sm'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
                )}
                {!workflowProcessData && !isError && (typeof content === 'string') && (
                  <Markdown content={content} />
                )}
              </div>
            </div>
            <div className='flex items-center justify-between mt-3'>
              <div className='flex items-center'>
                {
                  !isInWebApp && !isInstalledApp && !isResponding && (
                    <SimpleBtn
                      isDisabled={isError || !messageId}
                      className={cn(isMobile && '!px-1.5', 'space-x-1 mr-1')}
                      onClick={handleOpenLogModal}>
                      <File02 className='w-3.5 h-3.5' />
                      {!isMobile && <div>{t('common.operation.log')}</div>}
                    </SimpleBtn>
                  )
                }
                {((currentTab === 'RESULT' && workflowProcessData?.resultText) || !isWorkflow) && (
                  <SimpleBtn
                    isDisabled={isError || !messageId}
                    className={cn(isMobile && '!px-1.5', 'space-x-1')}
                    onClick={() => {
                      const copyContent = isWorkflow ? workflowProcessData?.resultText : content
                      if (typeof copyContent === 'string')
                        copy(copyContent)
@@ -316,68 +352,117 @@
                        copy(JSON.stringify(copyContent))
                      Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
                    }}>
                      <RiClipboardLine className='h-4 w-4' />
                    </ActionButton>
                  )}
                  {isInWebApp && isError && (
                    <ActionButton onClick={onRetry}>
                      <RiReplay15Line className='h-4 w-4' />
                    </ActionButton>
                  )}
                  {isInWebApp && !isWorkflow && (
                    <ActionButton disabled={isError || !messageId} onClick={() => { onSave?.(messageId as string) }}>
                      <RiBookmark3Line className='h-4 w-4' />
                    </ActionButton>
                  )}
                </div>
                {(supportFeedback || isInWebApp) && !isWorkflow && !isError && messageId && (
                  <div className='ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-sm'>
                    {!feedback?.rating && (
                      <>
                        <ActionButton onClick={() => onFeedback?.({ rating: 'like' })}>
                          <RiThumbUpLine className='h-4 w-4' />
                        </ActionButton>
                        <ActionButton onClick={() => onFeedback?.({ rating: 'dislike' })}>
                          <RiThumbDownLine className='h-4 w-4' />
                        </ActionButton>
                      </>
                    <RiClipboardLine className='w-3.5 h-3.5' />
                    {!isMobile && <div>{t('common.operation.copy')}</div>}
                  </SimpleBtn>
                )}
                {isInWebApp && (
                  <>
                    {!isWorkflow && (
                      <SimpleBtn
                        isDisabled={isError || !messageId}
                        className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
                        onClick={() => { onSave?.(messageId as string) }}
                      >
                        <Bookmark className='w-3.5 h-3.5' />
                        {!isMobile && <div>{t('common.operation.save')}</div>}
                      </SimpleBtn>
                    )}
                    {feedback?.rating === 'like' && (
                      <ActionButton state={ActionButtonState.Active} onClick={() => onFeedback?.({ rating: null })}>
                        <RiThumbUpLine className='h-4 w-4' />
                      </ActionButton>
                    {(moreLikeThis && depth < MAX_DEPTH) && (
                      <SimpleBtn
                        isDisabled={isError || !messageId}
                        className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
                        onClick={handleMoreLikeThis}
                      >
                        <Stars02 className='w-3.5 h-3.5' />
                        {!isMobile && <div>{t('appDebug.feature.moreLikeThis.title')}</div>}
                      </SimpleBtn>
                    )}
                    {feedback?.rating === 'dislike' && (
                      <ActionButton state={ActionButtonState.Destructive} onClick={() => onFeedback?.({ rating: null })}>
                        <RiThumbDownLine className='h-4 w-4' />
                      </ActionButton>
                    {isError && (
                      <SimpleBtn
                        onClick={onRetry}
                        className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
                      >
                        <RefreshCcw01 className='w-3.5 h-3.5' />
                        {!isMobile && <div>{t('share.generation.batchFailed.retry')}</div>}
                      </SimpleBtn>
                    )}
                    {!isError && messageId && !isWorkflow && (
                      <div className="mx-3 w-[1px] h-[14px] bg-gray-200"></div>
                    )}
                    {ratingContent}
                  </>
                )}
                {supportAnnotation && (
                  <>
                    <div className='ml-2 mr-1 h-[14px] w-[1px] bg-gray-200'></div>
                    <AnnotationCtrlBtn
                      appId={appId!}
                      messageId={messageId!}
                      className='ml-1'
                      query={question}
                      answer={content}
                      // not support cache. So can not be cached
                      cached={false}
                      onAdded={() => {
                      }}
                      onEdit={() => setIsShowReplyModal(true)}
                      onRemoved={() => { }}
                    />
                  </>
                )}
                <EditReplyModal
                  appId={appId!}
                  messageId={messageId!}
                  isShow={isShowReplyModal}
                  onHide={() => setIsShowReplyModal(false)}
                  query={question}
                  answer={content}
                  onAdded={() => { }}
                  onEdited={() => { }}
                  createdAt={0}
                  onRemove={() => { }}
                  onlyEditResponse
                />
                {supportFeedback && (
                  <div className='ml-1'>
                    {ratingContent}
                  </div>
                )}
                {isShowTextToSpeech && (
                  <>
                    <div className='ml-2 mr-2 h-[14px] w-[1px] bg-gray-200'></div>
                    <AudioBtn
                      id={messageId!}
                      className={'mr-1'}
                      voice={config?.text_to_speech?.voice}
                    />
                  </>
                )}
              </div>
              <div>
                {!workflowProcessData && (
                  <div className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
                )}
              </div>
            </div>
            {/* more like this elements */}
            {!isTop && (
              <div className={cn(
                'absolute top-[-32px] flex h-[33px] w-4 justify-center',
                isMobile ? 'left-[17px]' : 'left-[50%] translate-x-[-50%]',
              )}>
                <div className='h-full w-0.5 bg-divider-regular'></div>
                <div className={cn(
                  'absolute left-0 flex h-4 w-4 items-center justify-center rounded-2xl border-[0.5px] border-divider-subtle bg-util-colors-blue-blue-500 shadow-xs',
                  isMobile ? 'top-[3.5px]' : 'top-2',
                )}>
                  <RiSparklingFill className='h-3 w-3 text-text-primary-on-surface' />
                </div>
              </div>
            )}
          </>
          </div>
        )}
      </div>
      {((childMessageId || isQuerying) && depth < 3) && (
        <GenerationItem {...childProps as any} />
        <div className='pl-4'>
          <GenerationItem {...childProps as any} />
        </div>
      )}
    </>
    </div>
  )
}
export default React.memo(GenerationItem)