feat: llm text stream support for workflow app (#3798)

Co-authored-by: JzoNg <jzongcode@gmail.com>
This commit is contained in:
takatost
2024-04-28 17:37:00 +08:00
committed by GitHub
parent 8e4989ed03
commit ff67a6d338
27 changed files with 549 additions and 58 deletions

View File

@@ -8,9 +8,8 @@ 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 PromptLog from '@/app/components/app/chat/log'
import ResultTab from './result-tab'
import { Markdown } from '@/app/components/base/markdown'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast'
import AudioBtn from '@/app/components/base/audio-btn'
@@ -26,7 +25,6 @@ import EditReplyModal from '@/app/components/app/annotation/edit-annotation-moda
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 { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
const MAX_DEPTH = 3
@@ -293,23 +291,17 @@ const GenerationItem: FC<IGenerationItemProps> = ({
<div className={`flex ${contentClassName}`}>
<div className='grow w-0'>
{workflowProcessData && (
<WorkflowProcessItem grayBg data={workflowProcessData} expand={workflowProcessData.expand} />
<WorkflowProcessItem grayBg hideInfo data={workflowProcessData} expand={workflowProcessData.expand} />
)}
{workflowProcessData && !isError && (
<ResultTab data={workflowProcessData} content={content} />
)}
{isError && (
<div className='text-gray-400 text-sm'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
)}
{!isError && (typeof content === 'string') && (
{!workflowProcessData && !isError && (typeof content === 'string') && (
<Markdown content={content} />
)}
{!isError && (typeof content !== 'string') && (
<CodeEditor
readOnly
title={<div/>}
language={CodeLanguage.json}
value={content}
isJSONStringifyBeauty
/>
)}
</div>
</div>
@@ -427,7 +419,11 @@ const GenerationItem: FC<IGenerationItemProps> = ({
</>
)}
</div>
<div className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
<div>
{!workflowProcessData && (
<div className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
)}
</div>
</div>
</div>

View File

@@ -0,0 +1,74 @@
import {
memo,
useEffect,
// useRef,
useState,
} from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
// import Loading from '@/app/components/base/loading'
import { Markdown } from '@/app/components/base/markdown'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import type { WorkflowProcess } from '@/app/components/base/chat/types'
// import { WorkflowRunningStatus } from '@/app/components/workflow/types'
const ResultTab = ({
data,
content,
}: {
data?: WorkflowProcess
content: any
}) => {
const { t } = useTranslation()
const [currentTab, setCurrentTab] = useState<string>('DETAIL')
const switchTab = async (tab: string) => {
setCurrentTab(tab)
}
useEffect(() => {
if (data?.resultText)
switchTab('RESULT')
else
switchTab('DETAIL')
}, [data?.resultText])
return (
<div className='grow relative flex flex-col'>
{data?.resultText && (
<div className='shrink-0 flex items-center mb-2 border-b-[0.5px] border-[rgba(0,0,0,0.05)]'>
<div
className={cn(
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
currentTab === 'RESULT' && '!border-[rgb(21,94,239)] text-gray-700',
)}
onClick={() => switchTab('RESULT')}
>{t('runLog.result')}</div>
<div
className={cn(
'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
currentTab === 'DETAIL' && '!border-[rgb(21,94,239)] text-gray-700',
)}
onClick={() => switchTab('DETAIL')}
>{t('runLog.detail')}</div>
</div>
)}
<div className={cn('grow bg-white')}>
{currentTab === 'RESULT' && (
<Markdown content={data?.resultText || ''} />
)}
{currentTab === 'DETAIL' && content && (
<CodeEditor
readOnly
title={<div>JSON OUTPUT</div>}
language={CodeLanguage.json}
value={content}
isJSONStringifyBeauty
/>
)}
</div>
</div>
)
}
export default memo(ResultTab)

View File

@@ -54,6 +54,7 @@ export type WorkflowProcess = {
status: WorkflowRunningStatus
tracing: NodeTracing[]
expand?: boolean // for UI
resultText?: string
}
export type ChatItem = IChatItem & {

View File

@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="image-indent-left">
<path id="Icon" d="M21 9.25H15M21 4H3M21 14.75H15M21 20H3M4.6 16H9.4C9.96005 16 10.2401 16 10.454 15.891C10.6422 15.7951 10.7951 15.6422 10.891 15.454C11 15.2401 11 14.9601 11 14.4V9.6C11 9.03995 11 8.75992 10.891 8.54601C10.7951 8.35785 10.6422 8.20487 10.454 8.10899C10.2401 8 9.96005 8 9.4 8H4.6C4.03995 8 3.75992 8 3.54601 8.10899C3.35785 8.20487 3.20487 8.35785 3.10899 8.54601C3 8.75992 3 9.03995 3 9.6V14.4C3 14.9601 3 15.2401 3.10899 15.454C3.20487 15.6422 3.35785 15.7951 3.54601 15.891C3.75992 16 4.03995 16 4.6 16Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 743 B

View File

@@ -0,0 +1,39 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "image-indent-left"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Icon",
"d": "M21 9.25H15M21 4H3M21 14.75H15M21 20H3M4.6 16H9.4C9.96005 16 10.2401 16 10.454 15.891C10.6422 15.7951 10.7951 15.6422 10.891 15.454C11 15.2401 11 14.9601 11 14.4V9.6C11 9.03995 11 8.75992 10.891 8.54601C10.7951 8.35785 10.6422 8.20487 10.454 8.10899C10.2401 8 9.96005 8 9.4 8H4.6C4.03995 8 3.75992 8 3.54601 8.10899C3.35785 8.20487 3.20487 8.35785 3.10899 8.54601C3 8.75992 3 9.03995 3 9.6V14.4C3 14.9601 3 15.2401 3.10899 15.454C3.20487 15.6422 3.35785 15.7951 3.54601 15.891C3.75992 16 4.03995 16 4.6 16Z",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
}
]
},
"name": "ImageIndentLeft"
}

View File

@@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './ImageIndentLeft.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'ImageIndentLeft'
export default Icon

View File

@@ -1,6 +1,7 @@
export { default as AlignLeft } from './AlignLeft'
export { default as BezierCurve03 } from './BezierCurve03'
export { default as Colors } from './Colors'
export { default as ImageIndentLeft } from './ImageIndentLeft'
export { default as LeftIndent02 } from './LeftIndent02'
export { default as LetterSpacing01 } from './LetterSpacing01'
export { default as TypeSquare } from './TypeSquare'

View File

@@ -201,6 +201,7 @@ const Result: FC<IResultProps> = ({
status: WorkflowRunningStatus.Running,
tracing: [],
expand: false,
resultText: '',
})
setRespondingFalse()
},
@@ -243,15 +244,25 @@ const Result: FC<IResultProps> = ({
}))
if (!data.outputs)
setCompletionRes('')
else if (Object.keys(data.outputs).length > 1)
setCompletionRes(data.outputs)
else
setCompletionRes(data.outputs[Object.keys(data.outputs)[0]])
setCompletionRes(data.outputs)
setRespondingFalse()
setMessageId(tempMessageId)
onCompleted(getCompletionRes(), taskId, true)
isEnd = true
},
onTextChunk: (params) => {
const { data: { text } } = params
setWorkflowProccessData(produce(getWorkflowProccessData()!, (draft) => {
draft.resultText += text
}))
},
onTextReplace: (params) => {
const { data: { text } } = params
setWorkflowProccessData(produce(getWorkflowProccessData()!, (draft) => {
draft.resultText = text
}))
},
},
isInstalledApp,
installedAppInfo?.id,

View File

@@ -124,6 +124,7 @@ export const useWorkflowRun = () => {
status: WorkflowRunningStatus.Running,
},
tracing: [],
resultText: '',
})
ssePost(
@@ -284,6 +285,27 @@ export const useWorkflowRun = () => {
if (onNodeFinished)
onNodeFinished(params)
},
onTextChunk: (params) => {
const { data: { text } } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.resultTabActive = true
draft.resultText += text
}))
},
onTextReplace: (params) => {
const { data: { text } } = params
const {
workflowRunningData,
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData(produce(workflowRunningData!, (draft) => {
draft.resultText = text
}))
},
...restCallback,
},
)

View File

@@ -1,12 +1,12 @@
import {
memo,
useEffect,
useRef,
// useRef,
useState,
} from 'react'
import cn from 'classnames'
import { useTranslation } from 'react-i18next'
import OutputPanel from '../run/output-panel'
import ResultText from '../run/result-text'
import ResultPanel from '../run/result-panel'
import TracingPanel from '../run/tracing-panel'
import {
@@ -32,22 +32,15 @@ const WorkflowPreview = () => {
setCurrentTab(tab)
}
const [height, setHieght] = useState(0)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (showDebugAndPreviewPanel && showInputsPanel)
setCurrentTab('INPUT')
}, [showDebugAndPreviewPanel, showInputsPanel])
const adjustResultHeight = () => {
if (ref.current)
setHieght(ref.current?.clientHeight - 16 - 16 - 2 - 1)
}
useEffect(() => {
adjustResultHeight()
}, [])
if ((workflowRunningData?.result.status === WorkflowRunningStatus.Succeeded || workflowRunningData?.result.status === WorkflowRunningStatus.Failed) && !workflowRunningData.resultText)
switchTab('DETAIL')
}, [workflowRunningData])
return (
<div className={`
@@ -107,7 +100,7 @@ const WorkflowPreview = () => {
}}
>{t('runLog.tracing')}</div>
</div>
<div ref={ref} className={cn(
<div className={cn(
'grow bg-white h-0 overflow-y-auto rounded-b-2xl',
(currentTab === 'RESULT' || currentTab === 'TRACING') && '!bg-gray-50',
)}>
@@ -115,11 +108,11 @@ const WorkflowPreview = () => {
<InputsPanel onRun={() => switchTab('RESULT')} />
)}
{currentTab === 'RESULT' && (
<OutputPanel
<ResultText
isRunning={workflowRunningData?.result?.status === WorkflowRunningStatus.Running || !workflowRunningData?.result}
outputs={workflowRunningData?.result?.outputs}
outputs={workflowRunningData?.resultText}
error={workflowRunningData?.result?.error}
height={height}
onClick={() => switchTab('DETAIL')}
/>
)}
{currentTab === 'DETAIL' && (

View File

@@ -0,0 +1,56 @@
'use client'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { ImageIndentLeft } from '@/app/components/base/icons/src/vender/line/editor'
import { Markdown } from '@/app/components/base/markdown'
import LoadingAnim from '@/app/components/app/chat/loading-anim'
type ResultTextProps = {
isRunning?: boolean
outputs?: any
error?: string
onClick?: () => void
}
const ResultText: FC<ResultTextProps> = ({
isRunning,
outputs,
error,
onClick,
}) => {
const { t } = useTranslation()
return (
<div className='bg-gray-50 py-2'>
{isRunning && !outputs && (
<div className='pt-4 pl-[26px]'>
<LoadingAnim type='text' />
</div>
)}
{!isRunning && error && (
<div className='px-4'>
<div className='px-3 py-[10px] rounded-lg !bg-[#fef3f2] border-[0.5px] border-[rbga(0,0,0,0.05)] shadow-xs'>
<div className='text-xs leading-[18px] text-[#d92d20]'>{error}</div>
</div>
</div>
)}
{!isRunning && !outputs && !error && (
<div className='mt-[120px] px-4 py-2 flex flex-col items-center text-[13px] leading-[18px] text-gray-500'>
<ImageIndentLeft className='w-6 h-6 text-gray-400' />
<div className='mr-2'>{t('runLog.resultEmpty.title')}</div>
<div>
{t('runLog.resultEmpty.tipLeft')}
<span onClick={onClick} className='cursor-pointer text-primary-600'>{t('runLog.resultEmpty.link')}</span>
{t('runLog.resultEmpty.tipRight')}
</div>
</div>
)}
{outputs && (
<div className='px-4 py-2'>
<Markdown content={outputs} />
</div>
)}
</div>
)
}
export default ResultText

View File

@@ -19,11 +19,16 @@ import type {
} from './types'
import { WorkflowContext } from './context'
type PreviewRunningData = WorkflowRunningData & {
resultTabActive?: boolean
resultText?: string
}
type Shape = {
appId: string
panelWidth: number
workflowRunningData?: WorkflowRunningData
setWorkflowRunningData: (workflowData?: WorkflowRunningData) => void
workflowRunningData?: PreviewRunningData
setWorkflowRunningData: (workflowData: PreviewRunningData) => void
historyWorkflowData?: HistoryWorkflowData
setHistoryWorkflowData: (historyWorkflowData?: HistoryWorkflowData) => void
showRunHistory: boolean