Refactor text generation item component

This commit is contained in:
yyh 2025-12-11 16:06:38 +08:00
parent f20a2d1586
commit 2088660f55
No known key found for this signature in database
4 changed files with 496 additions and 254 deletions

View File

@ -0,0 +1,112 @@
import type { FC } from 'react'
import type { TFunction } from 'i18next'
import { RiPlayList2Line } from '@remixicon/react'
import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process'
import type { WorkflowProcess } from '@/app/components/base/chat/types'
import { Markdown } from '@/app/components/base/markdown'
import type { SiteInfo } from '@/models/share'
import cn from '@/utils/classnames'
import ResultTab from './result-tab'
type ContentSectionProps = {
workflowProcessData?: WorkflowProcess
taskId?: string
depth: number
isError: boolean
content: any
hideProcessDetail?: boolean
siteInfo: SiteInfo | null
currentTab: 'DETAIL' | 'RESULT'
onSwitchTab: (tab: 'DETAIL' | 'RESULT') => void
showResultTabs: boolean
t: TFunction
inSidePanel?: boolean
}
const ContentSection: FC<ContentSectionProps> = ({
workflowProcessData,
taskId,
depth,
isError,
content,
hideProcessDetail,
siteInfo,
currentTab,
onSwitchTab,
showResultTabs,
t,
inSidePanel,
}) => {
return (
<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={() => onSwitchTab('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={() => onSwitchTab('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>
)
}
export default ContentSection

View File

@ -0,0 +1,67 @@
import { useBoolean } from 'ahooks'
import { useEffect, useState } from 'react'
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import type { WorkflowProcess } from '@/app/components/base/chat/types'
type UseMoreLikeThisStateParams = {
controlClearMoreLikeThis?: number
isLoading?: boolean
}
export const useMoreLikeThisState = ({
controlClearMoreLikeThis,
isLoading,
}: UseMoreLikeThisStateParams) => {
const [completionRes, setCompletionRes] = useState('')
const [childMessageId, setChildMessageId] = useState<string | null>(null)
const [childFeedback, setChildFeedback] = useState<FeedbackType>({
rating: null,
})
const [isQuerying, { setTrue: startQuerying, setFalse: stopQuerying }] = useBoolean(false)
useEffect(() => {
if (controlClearMoreLikeThis) {
setChildMessageId(null)
setCompletionRes('')
}
}, [controlClearMoreLikeThis])
useEffect(() => {
if (isLoading)
setChildMessageId(null)
}, [isLoading])
return {
completionRes,
setCompletionRes,
childMessageId,
setChildMessageId,
childFeedback,
setChildFeedback,
isQuerying,
startQuerying,
stopQuerying,
}
}
export const useWorkflowTabs = (workflowProcessData?: WorkflowProcess) => {
const [currentTab, setCurrentTab] = useState<'DETAIL' | 'RESULT'>('DETAIL')
const showResultTabs = !!workflowProcessData?.resultText || !!workflowProcessData?.files?.length
useEffect(() => {
if (showResultTabs)
setCurrentTab('RESULT')
else
setCurrentTab('DETAIL')
}, [
showResultTabs,
workflowProcessData?.resultText,
workflowProcessData?.files?.length,
])
return {
currentTab,
setCurrentTab,
showResultTabs,
}
}

View File

@ -1,36 +1,23 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useState } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import {
RiBookmark3Line,
RiClipboardLine,
RiFileList3Line,
RiPlayList2Line,
RiReplay15Line,
RiSparklingFill,
RiSparklingLine,
RiThumbDownLine,
RiThumbUpLine,
} from '@remixicon/react'
import { RiSparklingFill } from '@remixicon/react'
import copy from 'copy-to-clipboard'
import { useParams } from 'next/navigation'
import { useBoolean } from 'ahooks'
import ResultTab from './result-tab'
import { Markdown } from '@/app/components/base/markdown'
import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast'
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
import { fetchTextGenerationMessage } from '@/service/debug'
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'
import ContentSection from './content-section'
import MetaSection from './meta-section'
import { useMoreLikeThisState, useWorkflowTabs } from './hooks'
const MAX_DEPTH = 3
@ -69,6 +56,35 @@ export const copyIcon = (
</svg>
)
const formatLogItem = (data: any) => {
if (Array.isArray(data.message)) {
const assistantLog = data.message[data.message.length - 1]?.role !== 'assistant'
? [{
role: 'assistant',
text: data.answer,
files: data.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
}]
: []
return {
...data,
log: [
...data.message,
...assistantLog,
],
}
}
const message = typeof data.message === 'string'
? { text: data.message }
: data.message
return {
...data,
log: [message],
}
}
const GenerationItem: FC<IGenerationItemProps> = ({
isWorkflow,
workflowProcessData,
@ -99,33 +115,97 @@ const GenerationItem: FC<IGenerationItemProps> = ({
const { t } = useTranslation()
const params = useParams()
const isTop = depth === 1
const [completionRes, setCompletionRes] = useState('')
const [childMessageId, setChildMessageId] = useState<string | null>(null)
const [childFeedback, setChildFeedback] = useState<FeedbackType>({
rating: null,
})
const {
config,
} = useChatContext()
completionRes,
setCompletionRes,
childMessageId,
setChildMessageId,
childFeedback,
setChildFeedback,
isQuerying,
startQuerying,
stopQuerying,
} = useMoreLikeThisState({ controlClearMoreLikeThis, isLoading })
const { currentTab, setCurrentTab, showResultTabs } = useWorkflowTabs(workflowProcessData)
const { config } = useChatContext()
const setCurrentLogItem = useAppStore(s => s.setCurrentLogItem)
const setShowPromptLogModal = useAppStore(s => s.setShowPromptLogModal)
const handleFeedback = async (childFeedback: FeedbackType) => {
await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, isInstalledApp, installedAppId)
setChildFeedback(childFeedback)
}
const handleFeedback = useCallback(async (nextFeedback: FeedbackType) => {
if (!childMessageId)
return
await updateFeedback(
{ url: `/messages/${childMessageId}/feedbacks`, body: { rating: nextFeedback.rating } },
isInstalledApp,
installedAppId,
)
setChildFeedback(nextFeedback)
}, [childMessageId, installedAppId, isInstalledApp, setChildFeedback])
const [isQuerying, { setTrue: startQuerying, setFalse: stopQuerying }] = useBoolean(false)
const handleMoreLikeThis = useCallback(async () => {
if (isQuerying || !messageId) {
Toast.notify({ type: 'warning', message: t('appDebug.errorMessage.waitForResponse') })
return
}
startQuerying()
try {
const res: any = await fetchMoreLikeThis(messageId as string, isInstalledApp, installedAppId)
setCompletionRes(res.answer)
setChildFeedback({ rating: null })
setChildMessageId(res.id)
}
finally {
stopQuerying()
}
}, [
installedAppId,
isInstalledApp,
isQuerying,
messageId,
setChildFeedback,
setChildMessageId,
setCompletionRes,
startQuerying,
stopQuerying,
t,
])
const childProps = {
isInWebApp: true,
const handleOpenLogModal = useCallback(async () => {
if (!messageId)
return
const data = await fetchTextGenerationMessage({
appId: params.appId as string,
messageId,
})
const logItem = formatLogItem(data)
setCurrentLogItem(logItem)
setShowPromptLogModal(true)
}, [messageId, params.appId, setCurrentLogItem, setShowPromptLogModal])
const copyContent = isWorkflow ? workflowProcessData?.resultText : content
const handleCopy = useCallback(() => {
if (typeof copyContent === 'string')
copy(copyContent)
else
copy(JSON.stringify(copyContent))
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}, [copyContent, t])
const shouldIndentForChild = Boolean(isMobile && (childMessageId || isQuerying) && depth < MAX_DEPTH)
const shouldRenderChild = (childMessageId || isQuerying) && depth < MAX_DEPTH
const canCopy = (currentTab === 'RESULT' && workflowProcessData?.resultText) || !isWorkflow
const childProps: IGenerationItemProps = {
isWorkflow,
className,
isError: false,
onRetry,
content: completionRes,
messageId: childMessageId,
depth: depth + 1,
moreLikeThis: true,
onFeedback: handleFeedback,
isLoading: isQuerying,
isResponding,
moreLikeThis: true,
depth: depth + 1,
onFeedback: handleFeedback,
feedback: childFeedback,
onSave,
isShowTextToSpeech,
@ -133,80 +213,13 @@ const GenerationItem: FC<IGenerationItemProps> = ({
isInstalledApp,
installedAppId,
controlClearMoreLikeThis,
isWorkflow,
isInWebApp: true,
siteInfo,
taskId,
inSidePanel,
hideProcessDetail,
}
const handleMoreLikeThis = async () => {
if (isQuerying || !messageId) {
Toast.notify({ type: 'warning', message: t('appDebug.errorMessage.waitForResponse') })
return
}
startQuerying()
const res: any = await fetchMoreLikeThis(messageId as string, isInstalledApp, installedAppId)
setCompletionRes(res.answer)
setChildFeedback({
rating: null,
})
setChildMessageId(res.id)
stopQuerying()
}
useEffect(() => {
if (controlClearMoreLikeThis) {
setChildMessageId(null)
setCompletionRes('')
}
}, [controlClearMoreLikeThis])
// regeneration clear child
useEffect(() => {
if (isLoading)
setChildMessageId(null)
}, [isLoading])
const handleOpenLogModal = async () => {
const data = await fetchTextGenerationMessage({
appId: params.appId as string,
messageId: messageId!,
})
const logItem = Array.isArray(data.message) ? {
...data,
log: [
...data.message,
...(data.message[data.message.length - 1].role !== 'assistant'
? [
{
role: 'assistant',
text: data.answer,
files: data.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
},
]
: []),
],
} : {
...data,
log: [typeof data.message === 'string' ? {
text: data.message,
} : data.message],
}
setCurrentLogItem(logItem)
setShowPromptLogModal(true)
}
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)}>
@ -215,152 +228,45 @@ const GenerationItem: FC<IGenerationItemProps> = ({
)}
{!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 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={() => {
const copyContent = isWorkflow ? workflowProcessData?.resultText : content
if (typeof copyContent === 'string')
copy(copyContent)
else
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>
</>
)}
{feedback?.rating === 'like' && (
<ActionButton state={ActionButtonState.Active} onClick={() => onFeedback?.({ rating: null })}>
<RiThumbUpLine className='h-4 w-4' />
</ActionButton>
)}
{feedback?.rating === 'dislike' && (
<ActionButton state={ActionButtonState.Destructive} onClick={() => onFeedback?.({ rating: null })}>
<RiThumbDownLine className='h-4 w-4' />
</ActionButton>
)}
</div>
)}
</div>
</div>
<ContentSection
workflowProcessData={workflowProcessData}
taskId={taskId}
depth={depth}
isError={isError}
content={content}
hideProcessDetail={hideProcessDetail}
siteInfo={siteInfo}
currentTab={currentTab}
onSwitchTab={setCurrentTab}
showResultTabs={showResultTabs}
t={t}
inSidePanel={inSidePanel}
/>
<MetaSection
showCharCount={!isWorkflow}
charCount={content?.length}
t={t}
shouldIndentForChild={shouldIndentForChild}
isInWebApp={isInWebApp}
isInstalledApp={isInstalledApp}
isResponding={isResponding}
isError={isError}
messageId={messageId}
onOpenLogModal={handleOpenLogModal}
moreLikeThis={moreLikeThis}
onMoreLikeThis={handleMoreLikeThis}
disableMoreLikeThis={depth === MAX_DEPTH}
isShowTextToSpeech={isShowTextToSpeech}
textToSpeechVoice={config?.text_to_speech?.voice}
canCopy={!!canCopy}
onCopy={handleCopy}
onRetry={onRetry}
isWorkflow={isWorkflow}
onSave={onSave}
feedback={feedback}
onFeedback={onFeedback}
supportFeedback={supportFeedback}
/>
{/* more like this elements */}
{!isTop && (
<div className={cn(
@ -379,8 +285,8 @@ const GenerationItem: FC<IGenerationItemProps> = ({
</>
)}
</div>
{((childMessageId || isQuerying) && depth < 3) && (
<GenerationItem {...childProps as any} />
{shouldRenderChild && (
<GenerationItem {...childProps} />
)}
</>
)

View File

@ -0,0 +1,157 @@
import type { FC } from 'react'
import type { TFunction } from 'i18next'
import {
RiBookmark3Line,
RiClipboardLine,
RiFileList3Line,
RiReplay15Line,
RiSparklingLine,
RiThumbDownLine,
RiThumbUpLine,
} from '@remixicon/react'
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import NewAudioButton from '@/app/components/base/new-audio-button'
import cn from '@/utils/classnames'
type FeedbackActionsProps = {
feedback?: FeedbackType
onFeedback?: (feedback: FeedbackType) => void
}
const FeedbackActions: FC<FeedbackActionsProps> = ({
feedback,
onFeedback,
}) => {
if (!feedback?.rating) {
return (
<>
<ActionButton onClick={() => onFeedback?.({ rating: 'like' })}>
<RiThumbUpLine className='h-4 w-4' />
</ActionButton>
<ActionButton onClick={() => onFeedback?.({ rating: 'dislike' })}>
<RiThumbDownLine className='h-4 w-4' />
</ActionButton>
</>
)
}
if (feedback.rating === 'like') {
return (
<ActionButton state={ActionButtonState.Active} onClick={() => onFeedback?.({ rating: null })}>
<RiThumbUpLine className='h-4 w-4' />
</ActionButton>
)
}
return (
<ActionButton state={ActionButtonState.Destructive} onClick={() => onFeedback?.({ rating: null })}>
<RiThumbDownLine className='h-4 w-4' />
</ActionButton>
)
}
type MetaSectionProps = {
showCharCount: boolean
charCount?: number
t: TFunction
shouldIndentForChild: boolean
isInWebApp?: boolean
isInstalledApp: boolean
isResponding?: boolean
isError: boolean
messageId?: string | null
onOpenLogModal: () => void
moreLikeThis?: boolean
onMoreLikeThis: () => void
disableMoreLikeThis: boolean
isShowTextToSpeech?: boolean
textToSpeechVoice?: string
canCopy: boolean
onCopy: () => void
onRetry: () => void
isWorkflow?: boolean
onSave?: (messageId: string) => void
feedback?: FeedbackType
onFeedback?: (feedback: FeedbackType) => void
supportFeedback?: boolean
}
const MetaSection: FC<MetaSectionProps> = ({
showCharCount,
charCount,
t,
shouldIndentForChild,
isInWebApp,
isInstalledApp,
isResponding,
isError,
messageId,
onOpenLogModal,
moreLikeThis,
onMoreLikeThis,
disableMoreLikeThis,
isShowTextToSpeech,
textToSpeechVoice,
canCopy,
onCopy,
onRetry,
isWorkflow,
onSave,
feedback,
onFeedback,
supportFeedback,
}) => {
return (
<div className={cn(
'system-xs-regular relative mt-1 h-4 px-4 text-text-quaternary',
shouldIndentForChild && 'pl-10',
)}>
{showCharCount && <span>{charCount} {t('common.unit.char')}</span>}
<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={onOpenLogModal}>
<RiFileList3Line className='h-4 w-4' />
</ActionButton>
</div>
)}
<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={disableMoreLikeThis ? ActionButtonState.Disabled : ActionButtonState.Default} disabled={disableMoreLikeThis} onClick={onMoreLikeThis}>
<RiSparklingLine className='h-4 w-4' />
</ActionButton>
)}
{isShowTextToSpeech && messageId && (
<NewAudioButton
id={messageId}
voice={textToSpeechVoice}
/>
)}
{canCopy && (
<ActionButton disabled={isError || !messageId} onClick={onCopy}>
<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'>
<FeedbackActions feedback={feedback} onFeedback={onFeedback} />
</div>
)}
</div>
</div>
)
}
export default MetaSection