From a430284aa21e3ae1f0d5654e55b2ad2852519cc2 Mon Sep 17 00:00:00 2001 From: wwf <yearningwang@iqtogether.com> Date: 星期三, 04 六月 2025 15:17:49 +0800 Subject: [PATCH] 初始化 --- app/components/app/text-generate/item/index.tsx | 449 +++++++++++++++++++++++++++++++++---------------------- 1 files changed, 267 insertions(+), 182 deletions(-) diff --git a/app/components/app/text-generate/item/index.tsx b/app/components/app/text-generate/item/index.tsx index aa3ffa3..3e2f837 100644 --- a/app/components/app/text-generate/item/index.tsx +++ b/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) -- Gitblit v1.8.0