mirror of
https://github.com/langgenius/dify.git
synced 2025-12-06 07:43:33 +00:00
feat: add a stop run button to the published app UI (#27509)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
This commit is contained in:
parent
a6c6bcf95c
commit
6d3ed468d8
@ -125,6 +125,12 @@ const TextGeneration: FC<IMainProps> = ({
|
|||||||
transfer_methods: [TransferMethod.local_file],
|
transfer_methods: [TransferMethod.local_file],
|
||||||
})
|
})
|
||||||
const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([])
|
const [completionFiles, setCompletionFiles] = useState<VisionFile[]>([])
|
||||||
|
const [runControl, setRunControl] = useState<{ onStop: () => Promise<void> | void; isStopping: boolean } | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCallBatchAPI)
|
||||||
|
setRunControl(null)
|
||||||
|
}, [isCallBatchAPI])
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
setIsCallBatchAPI(false)
|
setIsCallBatchAPI(false)
|
||||||
@ -417,6 +423,7 @@ const TextGeneration: FC<IMainProps> = ({
|
|||||||
isPC={isPC}
|
isPC={isPC}
|
||||||
isMobile={!isPC}
|
isMobile={!isPC}
|
||||||
isInstalledApp={isInstalledApp}
|
isInstalledApp={isInstalledApp}
|
||||||
|
appId={appId}
|
||||||
installedAppInfo={installedAppInfo}
|
installedAppInfo={installedAppInfo}
|
||||||
isError={task?.status === TaskStatus.failed}
|
isError={task?.status === TaskStatus.failed}
|
||||||
promptConfig={promptConfig}
|
promptConfig={promptConfig}
|
||||||
@ -434,6 +441,8 @@ const TextGeneration: FC<IMainProps> = ({
|
|||||||
isShowTextToSpeech={!!textToSpeechConfig?.enabled}
|
isShowTextToSpeech={!!textToSpeechConfig?.enabled}
|
||||||
siteInfo={siteInfo}
|
siteInfo={siteInfo}
|
||||||
onRunStart={() => setResultExisted(true)}
|
onRunStart={() => setResultExisted(true)}
|
||||||
|
onRunControlChange={!isCallBatchAPI ? setRunControl : undefined}
|
||||||
|
hideInlineStopButton={!isCallBatchAPI}
|
||||||
/>)
|
/>)
|
||||||
|
|
||||||
const renderBatchRes = () => {
|
const renderBatchRes = () => {
|
||||||
@ -565,6 +574,7 @@ const TextGeneration: FC<IMainProps> = ({
|
|||||||
onSend={handleSend}
|
onSend={handleSend}
|
||||||
visionConfig={visionConfig}
|
visionConfig={visionConfig}
|
||||||
onVisionFilesChange={setCompletionFiles}
|
onVisionFilesChange={setCompletionFiles}
|
||||||
|
runControl={runControl}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={cn(isInBatchTab ? 'block' : 'hidden')}>
|
<div className={cn(isInBatchTab ? 'block' : 'hidden')}>
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useBoolean } from 'ahooks'
|
import { useBoolean } from 'ahooks'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
import { produce } from 'immer'
|
import { produce } from 'immer'
|
||||||
import TextGenerationRes from '@/app/components/app/text-generate/item'
|
import TextGenerationRes from '@/app/components/app/text-generate/item'
|
||||||
import NoData from '@/app/components/share/text-generation/no-data'
|
import NoData from '@/app/components/share/text-generation/no-data'
|
||||||
import Toast from '@/app/components/base/toast'
|
import Toast from '@/app/components/base/toast'
|
||||||
import { sendCompletionMessage, sendWorkflowMessage, updateFeedback } from '@/service/share'
|
import Button from '@/app/components/base/button'
|
||||||
|
import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||||
|
import { RiLoader2Line } from '@remixicon/react'
|
||||||
|
import { sendCompletionMessage, sendWorkflowMessage, stopChatMessageResponding, stopWorkflowMessage, updateFeedback } from '@/service/share'
|
||||||
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import type { PromptConfig } from '@/models/debug'
|
import type { PromptConfig } from '@/models/debug'
|
||||||
@ -31,6 +34,7 @@ export type IResultProps = {
|
|||||||
isPC: boolean
|
isPC: boolean
|
||||||
isMobile: boolean
|
isMobile: boolean
|
||||||
isInstalledApp: boolean
|
isInstalledApp: boolean
|
||||||
|
appId: string
|
||||||
installedAppInfo?: InstalledApp
|
installedAppInfo?: InstalledApp
|
||||||
isError: boolean
|
isError: boolean
|
||||||
isShowTextToSpeech: boolean
|
isShowTextToSpeech: boolean
|
||||||
@ -48,6 +52,8 @@ export type IResultProps = {
|
|||||||
completionFiles: VisionFile[]
|
completionFiles: VisionFile[]
|
||||||
siteInfo: SiteInfo | null
|
siteInfo: SiteInfo | null
|
||||||
onRunStart: () => void
|
onRunStart: () => void
|
||||||
|
onRunControlChange?: (control: { onStop: () => Promise<void> | void; isStopping: boolean } | null) => void
|
||||||
|
hideInlineStopButton?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Result: FC<IResultProps> = ({
|
const Result: FC<IResultProps> = ({
|
||||||
@ -56,6 +62,7 @@ const Result: FC<IResultProps> = ({
|
|||||||
isPC,
|
isPC,
|
||||||
isMobile,
|
isMobile,
|
||||||
isInstalledApp,
|
isInstalledApp,
|
||||||
|
appId,
|
||||||
installedAppInfo,
|
installedAppInfo,
|
||||||
isError,
|
isError,
|
||||||
isShowTextToSpeech,
|
isShowTextToSpeech,
|
||||||
@ -73,13 +80,10 @@ const Result: FC<IResultProps> = ({
|
|||||||
completionFiles,
|
completionFiles,
|
||||||
siteInfo,
|
siteInfo,
|
||||||
onRunStart,
|
onRunStart,
|
||||||
|
onRunControlChange,
|
||||||
|
hideInlineStopButton = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [isResponding, { setTrue: setRespondingTrue, setFalse: setRespondingFalse }] = useBoolean(false)
|
const [isResponding, { setTrue: setRespondingTrue, setFalse: setRespondingFalse }] = useBoolean(false)
|
||||||
useEffect(() => {
|
|
||||||
if (controlStopResponding)
|
|
||||||
setRespondingFalse()
|
|
||||||
}, [controlStopResponding])
|
|
||||||
|
|
||||||
const [completionRes, doSetCompletionRes] = useState<string>('')
|
const [completionRes, doSetCompletionRes] = useState<string>('')
|
||||||
const completionResRef = useRef<string>('')
|
const completionResRef = useRef<string>('')
|
||||||
const setCompletionRes = (res: string) => {
|
const setCompletionRes = (res: string) => {
|
||||||
@ -94,6 +98,29 @@ const Result: FC<IResultProps> = ({
|
|||||||
doSetWorkflowProcessData(data)
|
doSetWorkflowProcessData(data)
|
||||||
}
|
}
|
||||||
const getWorkflowProcessData = () => workflowProcessDataRef.current
|
const getWorkflowProcessData = () => workflowProcessDataRef.current
|
||||||
|
const [currentTaskId, setCurrentTaskId] = useState<string | null>(null)
|
||||||
|
const [isStopping, setIsStopping] = useState(false)
|
||||||
|
const abortControllerRef = useRef<AbortController | null>(null)
|
||||||
|
const resetRunState = useCallback(() => {
|
||||||
|
setCurrentTaskId(null)
|
||||||
|
setIsStopping(false)
|
||||||
|
abortControllerRef.current = null
|
||||||
|
onRunControlChange?.(null)
|
||||||
|
}, [onRunControlChange])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const abortCurrentRequest = () => {
|
||||||
|
abortControllerRef.current?.abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controlStopResponding) {
|
||||||
|
abortCurrentRequest()
|
||||||
|
setRespondingFalse()
|
||||||
|
resetRunState()
|
||||||
|
}
|
||||||
|
|
||||||
|
return abortCurrentRequest
|
||||||
|
}, [controlStopResponding, resetRunState, setRespondingFalse])
|
||||||
|
|
||||||
const { notify } = Toast
|
const { notify } = Toast
|
||||||
const isNoData = !completionRes
|
const isNoData = !completionRes
|
||||||
@ -112,6 +139,40 @@ const Result: FC<IResultProps> = ({
|
|||||||
notify({ type: 'error', message })
|
notify({ type: 'error', message })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleStop = useCallback(async () => {
|
||||||
|
if (!currentTaskId || isStopping)
|
||||||
|
return
|
||||||
|
setIsStopping(true)
|
||||||
|
try {
|
||||||
|
if (isWorkflow)
|
||||||
|
await stopWorkflowMessage(appId, currentTaskId, isInstalledApp, installedAppInfo?.id || '')
|
||||||
|
else
|
||||||
|
await stopChatMessageResponding(appId, currentTaskId, isInstalledApp, installedAppInfo?.id || '')
|
||||||
|
abortControllerRef.current?.abort()
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
|
notify({ type: 'error', message })
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
setIsStopping(false)
|
||||||
|
}
|
||||||
|
}, [appId, currentTaskId, installedAppInfo?.id, isInstalledApp, isStopping, isWorkflow, notify])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!onRunControlChange)
|
||||||
|
return
|
||||||
|
if (isResponding && currentTaskId) {
|
||||||
|
onRunControlChange({
|
||||||
|
onStop: handleStop,
|
||||||
|
isStopping,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
onRunControlChange(null)
|
||||||
|
}
|
||||||
|
}, [currentTaskId, handleStop, isResponding, isStopping, onRunControlChange])
|
||||||
|
|
||||||
const checkCanSend = () => {
|
const checkCanSend = () => {
|
||||||
// batch will check outer
|
// batch will check outer
|
||||||
if (isCallBatchAPI)
|
if (isCallBatchAPI)
|
||||||
@ -196,6 +257,7 @@ const Result: FC<IResultProps> = ({
|
|||||||
rating: null,
|
rating: null,
|
||||||
})
|
})
|
||||||
setCompletionRes('')
|
setCompletionRes('')
|
||||||
|
resetRunState()
|
||||||
|
|
||||||
let res: string[] = []
|
let res: string[] = []
|
||||||
let tempMessageId = ''
|
let tempMessageId = ''
|
||||||
@ -213,6 +275,7 @@ const Result: FC<IResultProps> = ({
|
|||||||
if (!isEnd) {
|
if (!isEnd) {
|
||||||
setRespondingFalse()
|
setRespondingFalse()
|
||||||
onCompleted(getCompletionRes(), taskId, false)
|
onCompleted(getCompletionRes(), taskId, false)
|
||||||
|
resetRunState()
|
||||||
isTimeout = true
|
isTimeout = true
|
||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
@ -221,8 +284,10 @@ const Result: FC<IResultProps> = ({
|
|||||||
sendWorkflowMessage(
|
sendWorkflowMessage(
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
onWorkflowStarted: ({ workflow_run_id }) => {
|
onWorkflowStarted: ({ workflow_run_id, task_id }) => {
|
||||||
tempMessageId = workflow_run_id
|
tempMessageId = workflow_run_id
|
||||||
|
setCurrentTaskId(task_id || null)
|
||||||
|
setIsStopping(false)
|
||||||
setWorkflowProcessData({
|
setWorkflowProcessData({
|
||||||
status: WorkflowRunningStatus.Running,
|
status: WorkflowRunningStatus.Running,
|
||||||
tracing: [],
|
tracing: [],
|
||||||
@ -330,12 +395,38 @@ const Result: FC<IResultProps> = ({
|
|||||||
notify({ type: 'warning', message: t('appDebug.warningMessage.timeoutExceeded') })
|
notify({ type: 'warning', message: t('appDebug.warningMessage.timeoutExceeded') })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const workflowStatus = data.status as WorkflowRunningStatus | undefined
|
||||||
|
const markNodesStopped = (traces?: WorkflowProcess['tracing']) => {
|
||||||
|
if (!traces)
|
||||||
|
return
|
||||||
|
const markTrace = (trace: WorkflowProcess['tracing'][number]) => {
|
||||||
|
if ([NodeRunningStatus.Running, NodeRunningStatus.Waiting].includes(trace.status as NodeRunningStatus))
|
||||||
|
trace.status = NodeRunningStatus.Stopped
|
||||||
|
trace.details?.forEach(detailGroup => detailGroup.forEach(markTrace))
|
||||||
|
trace.retryDetail?.forEach(markTrace)
|
||||||
|
trace.parallelDetail?.children?.forEach(markTrace)
|
||||||
|
}
|
||||||
|
traces.forEach(markTrace)
|
||||||
|
}
|
||||||
|
if (workflowStatus === WorkflowRunningStatus.Stopped) {
|
||||||
|
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||||
|
draft.status = WorkflowRunningStatus.Stopped
|
||||||
|
markNodesStopped(draft.tracing)
|
||||||
|
}))
|
||||||
|
setRespondingFalse()
|
||||||
|
resetRunState()
|
||||||
|
onCompleted(getCompletionRes(), taskId, false)
|
||||||
|
isEnd = true
|
||||||
|
return
|
||||||
|
}
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
notify({ type: 'error', message: data.error })
|
notify({ type: 'error', message: data.error })
|
||||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||||
draft.status = WorkflowRunningStatus.Failed
|
draft.status = WorkflowRunningStatus.Failed
|
||||||
|
markNodesStopped(draft.tracing)
|
||||||
}))
|
}))
|
||||||
setRespondingFalse()
|
setRespondingFalse()
|
||||||
|
resetRunState()
|
||||||
onCompleted(getCompletionRes(), taskId, false)
|
onCompleted(getCompletionRes(), taskId, false)
|
||||||
isEnd = true
|
isEnd = true
|
||||||
return
|
return
|
||||||
@ -357,6 +448,7 @@ const Result: FC<IResultProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setRespondingFalse()
|
setRespondingFalse()
|
||||||
|
resetRunState()
|
||||||
setMessageId(tempMessageId)
|
setMessageId(tempMessageId)
|
||||||
onCompleted(getCompletionRes(), taskId, true)
|
onCompleted(getCompletionRes(), taskId, true)
|
||||||
isEnd = true
|
isEnd = true
|
||||||
@ -376,12 +468,19 @@ const Result: FC<IResultProps> = ({
|
|||||||
},
|
},
|
||||||
isInstalledApp,
|
isInstalledApp,
|
||||||
installedAppInfo?.id,
|
installedAppInfo?.id,
|
||||||
)
|
).catch((error) => {
|
||||||
|
setRespondingFalse()
|
||||||
|
resetRunState()
|
||||||
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
|
notify({ type: 'error', message })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
sendCompletionMessage(data, {
|
sendCompletionMessage(data, {
|
||||||
onData: (data: string, _isFirstMessage: boolean, { messageId }) => {
|
onData: (data: string, _isFirstMessage: boolean, { messageId, taskId }) => {
|
||||||
tempMessageId = messageId
|
tempMessageId = messageId
|
||||||
|
if (taskId && typeof taskId === 'string' && taskId.trim() !== '')
|
||||||
|
setCurrentTaskId(prev => prev ?? taskId)
|
||||||
res.push(data)
|
res.push(data)
|
||||||
setCompletionRes(res.join(''))
|
setCompletionRes(res.join(''))
|
||||||
},
|
},
|
||||||
@ -391,6 +490,7 @@ const Result: FC<IResultProps> = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
setRespondingFalse()
|
setRespondingFalse()
|
||||||
|
resetRunState()
|
||||||
setMessageId(tempMessageId)
|
setMessageId(tempMessageId)
|
||||||
onCompleted(getCompletionRes(), taskId, true)
|
onCompleted(getCompletionRes(), taskId, true)
|
||||||
isEnd = true
|
isEnd = true
|
||||||
@ -405,9 +505,13 @@ const Result: FC<IResultProps> = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
setRespondingFalse()
|
setRespondingFalse()
|
||||||
|
resetRunState()
|
||||||
onCompleted(getCompletionRes(), taskId, false)
|
onCompleted(getCompletionRes(), taskId, false)
|
||||||
isEnd = true
|
isEnd = true
|
||||||
},
|
},
|
||||||
|
getAbortController: (abortController) => {
|
||||||
|
abortControllerRef.current = abortController
|
||||||
|
},
|
||||||
}, isInstalledApp, installedAppInfo?.id)
|
}, isInstalledApp, installedAppInfo?.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -426,28 +530,46 @@ const Result: FC<IResultProps> = ({
|
|||||||
}, [controlRetry])
|
}, [controlRetry])
|
||||||
|
|
||||||
const renderTextGenerationRes = () => (
|
const renderTextGenerationRes = () => (
|
||||||
<TextGenerationRes
|
<>
|
||||||
isWorkflow={isWorkflow}
|
{!hideInlineStopButton && isResponding && currentTaskId && (
|
||||||
workflowProcessData={workflowProcessData}
|
<div className={`mb-3 flex ${isPC ? 'justify-end' : 'justify-center'}`}>
|
||||||
isError={isError}
|
<Button
|
||||||
onRetry={handleSend}
|
variant='secondary'
|
||||||
content={completionRes}
|
disabled={isStopping}
|
||||||
messageId={messageId}
|
onClick={handleStop}
|
||||||
isInWebApp
|
>
|
||||||
moreLikeThis={moreLikeThisEnabled}
|
{
|
||||||
onFeedback={handleFeedback}
|
isStopping
|
||||||
feedback={feedback}
|
? <RiLoader2Line className='mr-[5px] h-3.5 w-3.5 animate-spin' />
|
||||||
onSave={handleSaveMessage}
|
: <StopCircle className='mr-[5px] h-3.5 w-3.5' />
|
||||||
isMobile={isMobile}
|
}
|
||||||
isInstalledApp={isInstalledApp}
|
<span className='text-xs font-normal'>{t('appDebug.operation.stopResponding')}</span>
|
||||||
installedAppId={installedAppInfo?.id}
|
</Button>
|
||||||
isLoading={isCallBatchAPI ? (!completionRes && isResponding) : false}
|
</div>
|
||||||
taskId={isCallBatchAPI ? ((taskId as number) < 10 ? `0${taskId}` : `${taskId}`) : undefined}
|
)}
|
||||||
controlClearMoreLikeThis={controlClearMoreLikeThis}
|
<TextGenerationRes
|
||||||
isShowTextToSpeech={isShowTextToSpeech}
|
isWorkflow={isWorkflow}
|
||||||
hideProcessDetail
|
workflowProcessData={workflowProcessData}
|
||||||
siteInfo={siteInfo}
|
isError={isError}
|
||||||
/>
|
onRetry={handleSend}
|
||||||
|
content={completionRes}
|
||||||
|
messageId={messageId}
|
||||||
|
isInWebApp
|
||||||
|
moreLikeThis={moreLikeThisEnabled}
|
||||||
|
onFeedback={handleFeedback}
|
||||||
|
feedback={feedback}
|
||||||
|
onSave={handleSaveMessage}
|
||||||
|
isMobile={isMobile}
|
||||||
|
isInstalledApp={isInstalledApp}
|
||||||
|
installedAppId={installedAppInfo?.id}
|
||||||
|
isLoading={isCallBatchAPI ? (!completionRes && isResponding) : false}
|
||||||
|
taskId={isCallBatchAPI ? ((taskId as number) < 10 ? `0${taskId}` : `${taskId}`) : undefined}
|
||||||
|
controlClearMoreLikeThis={controlClearMoreLikeThis}
|
||||||
|
isShowTextToSpeech={isShowTextToSpeech}
|
||||||
|
hideProcessDetail
|
||||||
|
siteInfo={siteInfo}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import React, { useCallback } from 'react'
|
import React, { useCallback } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
|
RiLoader2Line,
|
||||||
RiPlayLargeLine,
|
RiPlayLargeLine,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import Select from '@/app/components/base/select'
|
import Select from '@/app/components/base/select'
|
||||||
@ -20,6 +21,7 @@ import cn from '@/utils/classnames'
|
|||||||
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
|
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
|
||||||
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
|
||||||
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
|
||||||
|
import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
|
||||||
|
|
||||||
export type IRunOnceProps = {
|
export type IRunOnceProps = {
|
||||||
siteInfo: SiteInfo
|
siteInfo: SiteInfo
|
||||||
@ -30,6 +32,10 @@ export type IRunOnceProps = {
|
|||||||
onSend: () => void
|
onSend: () => void
|
||||||
visionConfig: VisionSettings
|
visionConfig: VisionSettings
|
||||||
onVisionFilesChange: (files: VisionFile[]) => void
|
onVisionFilesChange: (files: VisionFile[]) => void
|
||||||
|
runControl?: {
|
||||||
|
onStop: () => Promise<void> | void
|
||||||
|
isStopping: boolean
|
||||||
|
} | null
|
||||||
}
|
}
|
||||||
const RunOnce: FC<IRunOnceProps> = ({
|
const RunOnce: FC<IRunOnceProps> = ({
|
||||||
promptConfig,
|
promptConfig,
|
||||||
@ -39,6 +45,7 @@ const RunOnce: FC<IRunOnceProps> = ({
|
|||||||
onSend,
|
onSend,
|
||||||
visionConfig,
|
visionConfig,
|
||||||
onVisionFilesChange,
|
onVisionFilesChange,
|
||||||
|
runControl,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const media = useBreakpoints()
|
const media = useBreakpoints()
|
||||||
@ -62,6 +69,14 @@ const RunOnce: FC<IRunOnceProps> = ({
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
onSend()
|
onSend()
|
||||||
}
|
}
|
||||||
|
const isRunning = !!runControl
|
||||||
|
const stopLabel = t('share.generation.stopRun', { defaultValue: 'Stop Run' })
|
||||||
|
const handlePrimaryClick = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
if (!isRunning)
|
||||||
|
return
|
||||||
|
e.preventDefault()
|
||||||
|
runControl?.onStop?.()
|
||||||
|
}, [isRunning, runControl])
|
||||||
|
|
||||||
const handleInputsChange = useCallback((newInputs: Record<string, any>) => {
|
const handleInputsChange = useCallback((newInputs: Record<string, any>) => {
|
||||||
onInputsChange(newInputs)
|
onInputsChange(newInputs)
|
||||||
@ -211,12 +226,25 @@ const RunOnce: FC<IRunOnceProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className={cn(!isPC && 'grow')}
|
className={cn(!isPC && 'grow')}
|
||||||
type='submit'
|
type={isRunning ? 'button' : 'submit'}
|
||||||
variant="primary"
|
variant={isRunning ? 'secondary' : 'primary'}
|
||||||
disabled={false}
|
disabled={isRunning && runControl?.isStopping}
|
||||||
|
onClick={handlePrimaryClick}
|
||||||
>
|
>
|
||||||
<RiPlayLargeLine className="mr-1 h-4 w-4 shrink-0" aria-hidden="true" />
|
{isRunning ? (
|
||||||
<span className='text-[13px]'>{t('share.generation.run')}</span>
|
<>
|
||||||
|
{runControl?.isStopping
|
||||||
|
? <RiLoader2Line className='mr-1 h-4 w-4 shrink-0 animate-spin' aria-hidden="true" />
|
||||||
|
: <StopCircle className='mr-1 h-4 w-4 shrink-0' aria-hidden="true" />
|
||||||
|
}
|
||||||
|
<span className='text-[13px]'>{stopLabel}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<RiPlayLargeLine className="mr-1 h-4 w-4 shrink-0" aria-hidden="true" />
|
||||||
|
<span className='text-[13px]'>{t('share.generation.run')}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { produce } from 'immer'
|
|||||||
import { v4 as uuidV4 } from 'uuid'
|
import { v4 as uuidV4 } from 'uuid'
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||||
|
import type { Node } from '@/app/components/workflow/types'
|
||||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||||
import { useWorkflowUpdate } from '@/app/components/workflow/hooks/use-workflow-interactions'
|
import { useWorkflowUpdate } from '@/app/components/workflow/hooks/use-workflow-interactions'
|
||||||
import { useWorkflowRunEvent } from '@/app/components/workflow/hooks/use-workflow-run-event/use-workflow-run-event'
|
import { useWorkflowRunEvent } from '@/app/components/workflow/hooks/use-workflow-run-event/use-workflow-run-event'
|
||||||
@ -152,7 +153,7 @@ export const useWorkflowRun = () => {
|
|||||||
getNodes,
|
getNodes,
|
||||||
setNodes,
|
setNodes,
|
||||||
} = store.getState()
|
} = store.getState()
|
||||||
const newNodes = produce(getNodes(), (draft) => {
|
const newNodes = produce(getNodes(), (draft: Node[]) => {
|
||||||
draft.forEach((node) => {
|
draft.forEach((node) => {
|
||||||
node.data.selected = false
|
node.data.selected = false
|
||||||
node.data._runningStatus = undefined
|
node.data._runningStatus = undefined
|
||||||
|
|||||||
@ -63,6 +63,7 @@ const translation = {
|
|||||||
csvStructureTitle: 'The CSV file must conform to the following structure:',
|
csvStructureTitle: 'The CSV file must conform to the following structure:',
|
||||||
downloadTemplate: 'Download the template here',
|
downloadTemplate: 'Download the template here',
|
||||||
field: 'Field',
|
field: 'Field',
|
||||||
|
stopRun: 'Stop Run',
|
||||||
batchFailed: {
|
batchFailed: {
|
||||||
info: '{{num}} failed executions',
|
info: '{{num}} failed executions',
|
||||||
retry: 'Retry',
|
retry: 'Retry',
|
||||||
|
|||||||
@ -78,18 +78,19 @@ export const stopChatMessageResponding = async (appId: string, taskId: string, i
|
|||||||
return getAction('post', isInstalledApp)(getUrl(`chat-messages/${taskId}/stop`, isInstalledApp, installedAppId))
|
return getAction('post', isInstalledApp)(getUrl(`chat-messages/${taskId}/stop`, isInstalledApp, installedAppId))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendCompletionMessage = async (body: Record<string, any>, { onData, onCompleted, onError, onMessageReplace }: {
|
export const sendCompletionMessage = async (body: Record<string, any>, { onData, onCompleted, onError, onMessageReplace, getAbortController }: {
|
||||||
onData: IOnData
|
onData: IOnData
|
||||||
onCompleted: IOnCompleted
|
onCompleted: IOnCompleted
|
||||||
onError: IOnError
|
onError: IOnError
|
||||||
onMessageReplace: IOnMessageReplace
|
onMessageReplace: IOnMessageReplace
|
||||||
|
getAbortController?: (abortController: AbortController) => void
|
||||||
}, isInstalledApp: boolean, installedAppId = '') => {
|
}, isInstalledApp: boolean, installedAppId = '') => {
|
||||||
return ssePost(getUrl('completion-messages', isInstalledApp, installedAppId), {
|
return ssePost(getUrl('completion-messages', isInstalledApp, installedAppId), {
|
||||||
body: {
|
body: {
|
||||||
...body,
|
...body,
|
||||||
response_mode: 'streaming',
|
response_mode: 'streaming',
|
||||||
},
|
},
|
||||||
}, { onData, onCompleted, isPublicAPI: !isInstalledApp, onError, onMessageReplace })
|
}, { onData, onCompleted, isPublicAPI: !isInstalledApp, onError, onMessageReplace, getAbortController })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendWorkflowMessage = async (
|
export const sendWorkflowMessage = async (
|
||||||
@ -146,6 +147,12 @@ export const sendWorkflowMessage = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const stopWorkflowMessage = async (_appId: string, taskId: string, isInstalledApp: boolean, installedAppId = '') => {
|
||||||
|
if (!taskId)
|
||||||
|
return
|
||||||
|
return getAction('post', isInstalledApp)(getUrl(`workflows/tasks/${taskId}/stop`, isInstalledApp, installedAppId))
|
||||||
|
}
|
||||||
|
|
||||||
export const fetchAppInfo = async () => {
|
export const fetchAppInfo = async () => {
|
||||||
return get('/site') as Promise<AppData>
|
return get('/site') as Promise<AppData>
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user