diff --git a/web/app/components/app/text-generate/item/action-bar.tsx b/web/app/components/app/text-generate/item/action-bar.tsx new file mode 100644 index 0000000000..5ceb97ef12 --- /dev/null +++ b/web/app/components/app/text-generate/item/action-bar.tsx @@ -0,0 +1,153 @@ +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiBookmark3Line, + RiClipboardLine, + RiFileList3Line, + RiReplay15Line, + RiSparklingLine, + RiThumbDownLine, + RiThumbUpLine, +} from '@remixicon/react' +import copy from 'copy-to-clipboard' +import ActionButton, { ActionButtonState } from '@/app/components/base/action-button' +import NewAudioButton from '@/app/components/base/new-audio-button' +import Toast from '@/app/components/base/toast' +import type { FeedbackType } from '@/app/components/base/chat/chat/type' +import type { WorkflowProcess } from '@/app/components/base/chat/types' +import cn from '@/utils/classnames' +import { MAX_DEPTH, type WorkflowTab } from './hooks' + +type ActionBarProps = { + isWorkflow?: boolean + content: any + isMobile?: boolean + hasChildItem: boolean + isInWebApp?: boolean + isInstalledApp: boolean + isResponding?: boolean + isError: boolean + messageId?: string | null + moreLikeThis?: boolean + depth: number + onMoreLikeThis: () => void + onOpenLog: () => void + isShowTextToSpeech?: boolean + voiceId?: string + onRetry: () => void + onSave?: (messageId: string) => void + feedback?: FeedbackType + onFeedback?: (feedback: FeedbackType) => void + supportFeedback?: boolean + workflowProcessData?: WorkflowProcess + currentTab: WorkflowTab +} + +const ActionBar: FC = ({ + isWorkflow, + content, + isMobile, + hasChildItem, + isInWebApp, + isInstalledApp, + isResponding, + isError, + messageId, + moreLikeThis, + depth, + onMoreLikeThis, + onOpenLog, + isShowTextToSpeech, + voiceId, + onRetry, + onSave, + feedback, + onFeedback, + supportFeedback, + workflowProcessData, + currentTab, +}) => { + const { t } = useTranslation() + const showCopyButton = ((currentTab === 'RESULT' && workflowProcessData?.resultText) || !isWorkflow) && !isResponding + + const handleCopy = () => { + 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') }) + } + + return ( +
+ {!isWorkflow && {content?.length} {t('common.unit.char')}} +
+ {!isInWebApp && !isInstalledApp && !isResponding && ( +
+ + + +
+ )} +
+ {moreLikeThis && ( + + + + )} + {isShowTextToSpeech && messageId && ( + + )} + {showCopyButton && ( + + + + )} + {isInWebApp && isError && ( + + + + )} + {isInWebApp && !isWorkflow && ( + { onSave?.(messageId as string) }}> + + + )} +
+ {(supportFeedback || isInWebApp) && !isWorkflow && !isError && messageId && !isResponding && ( +
+ {!feedback?.rating && ( + <> + onFeedback?.({ rating: 'like' })}> + + + onFeedback?.({ rating: 'dislike' })}> + + + + )} + {feedback?.rating === 'like' && ( + onFeedback?.({ rating: null })}> + + + )} + {feedback?.rating === 'dislike' && ( + onFeedback?.({ rating: null })}> + + + )} +
+ )} +
+
+ ) +} + +export default ActionBar diff --git a/web/app/components/app/text-generate/item/hooks.ts b/web/app/components/app/text-generate/item/hooks.ts new file mode 100644 index 0000000000..2de20c8a72 --- /dev/null +++ b/web/app/components/app/text-generate/item/hooks.ts @@ -0,0 +1,85 @@ +import { useCallback, useEffect, useState } from 'react' +import { useBoolean } from 'ahooks' +import { useTranslation } from 'react-i18next' +import Toast from '@/app/components/base/toast' +import { fetchMoreLikeThis, updateFeedback } from '@/service/share' +import type { FeedbackType } from '@/app/components/base/chat/chat/type' +import type { WorkflowProcess } from '@/app/components/base/chat/types' + +export const MAX_DEPTH = 3 +export type WorkflowTab = 'RESULT' | 'DETAIL' + +export const useMoreLikeThis = ( + messageId: string | null | undefined, + isInstalledApp: boolean, + installedAppId: string | undefined, + controlClearMoreLikeThis: number | undefined, + isLoading: boolean | undefined, +) => { + const { t } = useTranslation() + const [completionRes, setCompletionRes] = useState('') + const [childMessageId, setChildMessageId] = useState(null) + const [childFeedback, setChildFeedback] = useState({ rating: null }) + 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, isInstalledApp, installedAppId) + setCompletionRes(res.answer) + setChildFeedback({ rating: null }) + setChildMessageId(res.id) + } + finally { + stopQuerying() + } + }, [isQuerying, messageId, t, startQuerying, isInstalledApp, installedAppId, stopQuerying]) + + const handleFeedback = useCallback(async (feedback: FeedbackType) => { + if (childMessageId) { + await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: feedback.rating } }, isInstalledApp, installedAppId) + setChildFeedback(feedback) + } + }, [childMessageId, isInstalledApp, installedAppId]) + + useEffect(() => { + if (controlClearMoreLikeThis) { + setChildMessageId(null) + setCompletionRes('') + } + }, [controlClearMoreLikeThis]) + + useEffect(() => { + if (isLoading) + setChildMessageId(null) + }, [isLoading]) + + return { + completionRes, + childMessageId, + childFeedback, + isQuerying, + handleMoreLikeThis, + handleFeedback, + } +} + +export const useWorkflowTabs = (workflowProcessData?: WorkflowProcess) => { + const [currentTab, setCurrentTab] = useState('DETAIL') + + useEffect(() => { + if (workflowProcessData?.resultText || !!workflowProcessData?.files?.length) + setCurrentTab('RESULT') + else + setCurrentTab('DETAIL') + }, [workflowProcessData?.files?.length, workflowProcessData?.resultText]) + + return { + currentTab, + setCurrentTab, + } +} diff --git a/web/app/components/app/text-generate/item/index.tsx b/web/app/components/app/text-generate/item/index.tsx index 92d86351e0..5b58e962fa 100644 --- a/web/app/components/app/text-generate/item/index.tsx +++ b/web/app/components/app/text-generate/item/index.tsx @@ -1,38 +1,24 @@ 'use client' import type { FC } from 'react' -import React, { useEffect, useState } from 'react' +import React 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 { 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' - -const MAX_DEPTH = 3 +import { MAX_DEPTH, useMoreLikeThis, useWorkflowTabs } from './hooks' +import ActionBar from './action-bar' +import WorkflowContent from './workflow-content' export type IGenerationItemProps = { isWorkflow?: boolean @@ -63,12 +49,6 @@ export type IGenerationItemProps = { inSidePanel?: boolean } -export const copyIcon = ( - - - -) - const GenerationItem: FC = ({ isWorkflow, workflowProcessData, @@ -99,11 +79,6 @@ const GenerationItem: FC = ({ const { t } = useTranslation() const params = useParams() const isTop = depth === 1 - const [completionRes, setCompletionRes] = useState('') - const [childMessageId, setChildMessageId] = useState(null) - const [childFeedback, setChildFeedback] = useState({ - rating: null, - }) const { config, } = useChatContext() @@ -111,12 +86,49 @@ const GenerationItem: FC = ({ 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 { + completionRes, + childMessageId, + childFeedback, + isQuerying, + handleMoreLikeThis, + handleFeedback, + } = useMoreLikeThis(messageId, isInstalledApp, installedAppId, controlClearMoreLikeThis, isLoading) - const [isQuerying, { setTrue: startQuerying, setFalse: stopQuerying }] = useBoolean(false) + const { currentTab, setCurrentTab } = useWorkflowTabs(workflowProcessData) + + 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 childProps = { isInWebApp: true, @@ -138,75 +150,6 @@ const GenerationItem: FC = ({ taskId, } - 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('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 ( <>
@@ -221,51 +164,16 @@ const GenerationItem: FC = ({ !inSidePanel && 'rounded-2xl border-t border-divider-subtle bg-chat-bubble-bg', )}> {workflowProcessData && ( - <> -
- {taskId && ( -
- - {t('share.generation.execution')} - · - {taskId} -
- )} - {siteInfo && workflowProcessData && ( - - )} - {showResultTabs && ( -
-
switchTab('RESULT')} - >{t('runLog.result')}
-
switchTab('DETAIL')} - >{t('runLog.detail')}
-
- )} -
- {!isError && ( - - )} - + )} {!workflowProcessData && taskId && (
@@ -284,83 +192,32 @@ const GenerationItem: FC = ({
)}
- {/* meta data */} -
- {!isWorkflow && {content?.length} {t('common.unit.char')}} - {/* action buttons */} -
- {!isInWebApp && !isInstalledApp && !isResponding && ( -
- - - {/*
{t('common.operation.log')}
*/} -
-
- )} -
- {moreLikeThis && ( - - - - )} - {isShowTextToSpeech && ( - - )} - {((currentTab === 'RESULT' && workflowProcessData?.resultText) || !isWorkflow) && ( - { - 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') }) - }}> - - - )} - {isInWebApp && isError && ( - - - - )} - {isInWebApp && !isWorkflow && ( - { onSave?.(messageId as string) }}> - - - )} -
- {(supportFeedback || isInWebApp) && !isWorkflow && !isError && messageId && ( -
- {!feedback?.rating && ( - <> - onFeedback?.({ rating: 'like' })}> - - - onFeedback?.({ rating: 'dislike' })}> - - - - )} - {feedback?.rating === 'like' && ( - onFeedback?.({ rating: null })}> - - - )} - {feedback?.rating === 'dislike' && ( - onFeedback?.({ rating: null })}> - - - )} -
- )} -
-
+ + + {/* more like this elements */} {!isTop && (
= ({ )}
- {((childMessageId || isQuerying) && depth < 3) && ( + {((childMessageId || isQuerying) && depth < MAX_DEPTH) && ( )} ) } + export default React.memo(GenerationItem) diff --git a/web/app/components/app/text-generate/item/workflow-content.tsx b/web/app/components/app/text-generate/item/workflow-content.tsx new file mode 100644 index 0000000000..b963330c4d --- /dev/null +++ b/web/app/components/app/text-generate/item/workflow-content.tsx @@ -0,0 +1,84 @@ +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { RiPlayList2Line } from '@remixicon/react' +import ResultTab from './result-tab' +import type { WorkflowTab } from './hooks' +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 cn from '@/utils/classnames' + +type WorkflowContentProps = { + workflowProcessData: WorkflowProcess + taskId?: string + isError: boolean + hideProcessDetail?: boolean + siteInfo: SiteInfo | null + currentTab: WorkflowTab + onSwitchTab: (tab: WorkflowTab) => void + content: any +} + +const WorkflowContent: FC = ({ + workflowProcessData, + taskId, + isError, + hideProcessDetail, + siteInfo, + currentTab, + onSwitchTab, + content, +}) => { + const { t } = useTranslation() + const showResultTabs = !!workflowProcessData?.resultText || !!workflowProcessData?.files?.length + + return ( + <> +
+ {taskId && ( +
+ + {t('share.generation.execution')} + · + {taskId} +
+ )} + {siteInfo && ( + + )} + {showResultTabs && ( +
+
onSwitchTab('RESULT')} + >{t('runLog.result')}
+
onSwitchTab('DETAIL')} + >{t('runLog.detail')}
+
+ )} +
+ {!isError && ( + + )} + + ) +} + +export default WorkflowContent