feat: batch run support retry errors and decrease rate limit times (#1215)

This commit is contained in:
Joel 2023-09-25 10:20:50 +08:00 committed by GitHub
parent 841e967d48
commit c40ee7e629
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 428 additions and 127 deletions

View File

@ -12,10 +12,15 @@ import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import type { Feedbacktype } from '@/app/components/app/chat/type' import type { Feedbacktype } from '@/app/components/app/chat/type'
import { fetchMoreLikeThis, updateFeedback } from '@/service/share' import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
import { Clipboard } 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'
const MAX_DEPTH = 3 const MAX_DEPTH = 3
export type IGenerationItemProps = { export type IGenerationItemProps = {
className?: string className?: string
isError: boolean
onRetry: () => void
content: string content: string
messageId?: string | null messageId?: string | null
isLoading?: boolean isLoading?: boolean
@ -32,14 +37,15 @@ export type IGenerationItemProps = {
controlClearMoreLikeThis?: number controlClearMoreLikeThis?: number
} }
export const SimpleBtn = ({ className, onClick, children }: { export const SimpleBtn = ({ className, isDisabled, onClick, children }: {
className?: string className?: string
isDisabled?: boolean
onClick?: () => void onClick?: () => void
children: React.ReactNode children: React.ReactNode
}) => ( }) => (
<div <div
className={cn(className, 'flex items-center h-7 px-3 rounded-md border border-gray-200 text-xs text-gray-700 font-medium cursor-pointer hover:shadow-sm hover:border-gray-300')} className={cn(className, 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')}
onClick={() => onClick?.()} onClick={() => !isDisabled && onClick?.()}
> >
{children} {children}
</div> </div>
@ -51,27 +57,10 @@ export const copyIcon = (
</svg> </svg>
) )
const moreLikeThisIcon = (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_4138_1944)">
<path d="M2.62533 12.8337V9.91699M2.62533 4.08366V1.16699M1.16699 2.62533H4.08366M1.16699 11.3753H4.08366M7.58366 1.75033L6.57206 4.38049C6.40755 4.80821 6.32529 5.02207 6.19738 5.20196C6.08402 5.36139 5.94472 5.50069 5.78529 5.61405C5.6054 5.74196 5.39155 5.82421 4.96383 5.98872L2.33366 7.00033L4.96383 8.01193C5.39155 8.17644 5.60541 8.25869 5.78529 8.3866C5.94472 8.49996 6.08402 8.63926 6.19738 8.79869C6.32529 8.97858 6.40755 9.19244 6.57206 9.62016L7.58366 12.2503L8.59526 9.62015C8.75977 9.19244 8.84202 8.97858 8.96993 8.79869C9.0833 8.63926 9.22259 8.49996 9.38203 8.3866C9.56191 8.25869 9.77577 8.17644 10.2035 8.01193L12.8337 7.00033L10.2035 5.98872C9.77577 5.82421 9.56191 5.74196 9.38203 5.61405C9.22259 5.50069 9.0833 5.36139 8.96993 5.20196C8.84202 5.02207 8.75977 4.80821 8.59526 4.38049L7.58366 1.75033Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
</g>
<defs>
<clipPath id="clip0_4138_1944">
<rect width="14" height="14" fill="white" />
</clipPath>
</defs>
</svg>
)
const saveIcon = (
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.0837 12.25L7.00033 9.33333L2.91699 12.25V2.91667C2.91699 2.60725 3.03991 2.3105 3.2587 2.09171C3.47749 1.87292 3.77424 1.75 4.08366 1.75H9.91699C10.2264 1.75 10.5232 1.87292 10.7419 2.09171C10.9607 2.3105 11.0837 2.60725 11.0837 2.91667V12.25Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
</svg>
)
const GenerationItem: FC<IGenerationItemProps> = ({ const GenerationItem: FC<IGenerationItemProps> = ({
className, className,
isError,
onRetry,
content, content,
messageId, messageId,
isLoading, isLoading,
@ -162,7 +151,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
}, [isLoading]) }, [isLoading])
return ( return (
<div className={cn(className, isTop ? 'rounded-xl border border-gray-200 bg-white' : 'rounded-br-xl !mt-0')} <div className={cn(className, isTop ? `rounded-xl border ${!isError ? 'border-gray-200 bg-white' : 'border-[#FECDCA] bg-[#FEF3F2]'} ` : 'rounded-br-xl !mt-0')}
style={isTop style={isTop
? { ? {
boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)', boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
@ -186,98 +175,112 @@ const GenerationItem: FC<IGenerationItemProps> = ({
} }
<div className='flex'> <div className='flex'>
<div className='grow w-0'> <div className='grow w-0'>
<Markdown content={content} /> {isError
? <div className='text-gray-400 text-sm'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
: (
<Markdown content={ content } />
)}
</div> </div>
</div> </div>
{messageId && (
<div className='flex items-center justify-between mt-3'> <div className='flex items-center justify-between mt-3'>
<div className='flex items-center'> <div className='flex items-center'>
<SimpleBtn <SimpleBtn
className={cn(isMobile && '!px-1.5', 'space-x-1')} isDisabled={isError || !messageId}
onClick={() => { className={cn(isMobile && '!px-1.5', 'space-x-1')}
copy(content) onClick={() => {
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') }) copy(content)
}}> Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
{copyIcon} }}>
{!isMobile && <div>{t('common.operation.copy')}</div>} <Clipboard className='w-3.5 h-3.5' />
</SimpleBtn> {!isMobile && <div>{t('common.operation.copy')}</div>}
{isInWebApp && ( </SimpleBtn>
<> {isInWebApp && (
<>
<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>
{(moreLikeThis && depth < MAX_DEPTH) && (
<SimpleBtn <SimpleBtn
isDisabled={isError || !messageId}
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')} className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
onClick={() => { onSave?.(messageId as string) }} onClick={handleMoreLikeThis}
> >
{saveIcon} <Stars02 className='w-3.5 h-3.5' />
{!isMobile && <div>{t('common.operation.save')}</div>} {!isMobile && <div>{t('appDebug.feature.moreLikeThis.title')}</div>}
</SimpleBtn>)}
{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 && <div className="mx-3 w-[1px] h-[14px] bg-gray-200"></div>}
{!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> </SimpleBtn>
{(moreLikeThis && depth < MAX_DEPTH) && ( )}
<SimpleBtn {!isError && messageId && feedback?.rating === 'like' && (
className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')} <div
onClick={handleMoreLikeThis} onClick={() => {
> onFeedback?.({
{moreLikeThisIcon} rating: null,
{!isMobile && <div>{t('appDebug.feature.moreLikeThis.title')}</div>} })
</SimpleBtn>)} }}
<div className="mx-3 w-[1px] h-[14px] bg-gray-200"></div> 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'>
{!feedback?.rating && ( <HandThumbUpIcon width={16} height={16} />
<SimpleBtn className="!px-0"> </div>
<> )}
<div {!isError && messageId && feedback?.rating === 'dislike' && (
onClick={() => { <div
onFeedback?.({ onClick={() => {
rating: 'like', onFeedback?.({
}) rating: null,
}} })
className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'> }}
<HandThumbUpIcon width={16} height={16} /> 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'>
</div> <HandThumbDownIcon 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>
)}
{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>
)}
{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>
)}
</>
)}
</div>
<div className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
</div> </div>
)} <div className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
</div>
</div> </div>
)} )}
{((childMessageId || isQuerying) && depth < 3) && ( {((childMessageId || isQuerying) && depth < 3) && (
<div className='pl-4'> <div className='pl-4'>
<GenerationItem {...childProps} /> <GenerationItem {...childProps as any} />
</div> </div>
)} )}

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 10C2 10 4.00498 7.26822 5.63384 5.63824C7.26269 4.00827 9.5136 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.89691 21 4.43511 18.2543 3.35177 14.5M2 10V4M2 10H8" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 376 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 4C16.93 4 17.395 4 17.7765 4.10222C18.8117 4.37962 19.6204 5.18827 19.8978 6.22354C20 6.60504 20 7.07003 20 8V17.2C20 18.8802 20 19.7202 19.673 20.362C19.3854 20.9265 18.9265 21.3854 18.362 21.673C17.7202 22 16.8802 22 15.2 22H8.8C7.11984 22 6.27976 22 5.63803 21.673C5.07354 21.3854 4.6146 20.9265 4.32698 20.362C4 19.7202 4 18.8802 4 17.2V8C4 7.07003 4 6.60504 4.10222 6.22354C4.37962 5.18827 5.18827 4.37962 6.22354 4.10222C6.60504 4 7.07003 4 8 4M9.6 6H14.4C14.9601 6 15.2401 6 15.454 5.89101C15.6422 5.79513 15.7951 5.64215 15.891 5.45399C16 5.24008 16 4.96005 16 4.4V3.6C16 3.03995 16 2.75992 15.891 2.54601C15.7951 2.35785 15.6422 2.20487 15.454 2.10899C15.2401 2 14.9601 2 14.4 2H9.6C9.03995 2 8.75992 2 8.54601 2.10899C8.35785 2.20487 8.20487 2.35785 8.10899 2.54601C8 2.75992 8 3.03995 8 3.6V4.4C8 4.96005 8 5.24008 8.10899 5.45399C8.20487 5.64215 8.35785 5.79513 8.54601 5.89101C8.75992 6 9.03995 6 9.6 6Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 7.8C5 6.11984 5 5.27976 5.32698 4.63803C5.6146 4.07354 6.07354 3.6146 6.63803 3.32698C7.27976 3 8.11984 3 9.8 3H14.2C15.8802 3 16.7202 3 17.362 3.32698C17.9265 3.6146 18.3854 4.07354 18.673 4.63803C19 5.27976 19 6.11984 19 7.8V21L12 17L5 21V7.8Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 443 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.5 22V17M4.5 7V2M2 4.5H7M2 19.5H7M13 3L11.2658 7.50886C10.9838 8.24209 10.8428 8.60871 10.6235 8.91709C10.4292 9.1904 10.1904 9.42919 9.91709 9.62353C9.60871 9.8428 9.24209 9.98381 8.50886 10.2658L4 12L8.50886 13.7342C9.24209 14.0162 9.60871 14.1572 9.91709 14.3765C10.1904 14.5708 10.4292 14.8096 10.6235 15.0829C10.8428 15.3913 10.9838 15.7579 11.2658 16.4911L13 21L14.7342 16.4911C15.0162 15.7579 15.1572 15.3913 15.3765 15.0829C15.5708 14.8096 15.8096 14.5708 16.0829 14.3765C16.3913 14.1572 16.7579 14.0162 17.4911 13.7342L22 12L17.4911 10.2658C16.7579 9.98381 16.3913 9.8428 16.0829 9.62353C15.8096 9.42919 15.5708 9.1904 15.3765 8.91709C15.1572 8.60871 15.0162 8.24209 14.7342 7.50886L13 3Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 894 B

View File

@ -0,0 +1,29 @@
{
"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": "path",
"attributes": {
"d": "M2 10C2 10 4.00498 7.26822 5.63384 5.63824C7.26269 4.00827 9.5136 3 12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.89691 21 4.43511 18.2543 3.35177 14.5M2 10V4M2 10H8",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "RefreshCcw01"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './RefreshCcw01.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 = 'RefreshCcw01'
export default Icon

View File

@ -3,4 +3,5 @@ export { default as ArrowUpRight } from './ArrowUpRight'
export { default as ChevronDownDouble } from './ChevronDownDouble' export { default as ChevronDownDouble } from './ChevronDownDouble'
export { default as ChevronDown } from './ChevronDown' export { default as ChevronDown } from './ChevronDown'
export { default as ChevronRight } from './ChevronRight' export { default as ChevronRight } from './ChevronRight'
export { default as RefreshCcw01 } from './RefreshCcw01'
export { default as RefreshCw05 } from './RefreshCw05' export { default as RefreshCw05 } from './RefreshCw05'

View File

@ -0,0 +1,29 @@
{
"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": "path",
"attributes": {
"d": "M16 4C16.93 4 17.395 4 17.7765 4.10222C18.8117 4.37962 19.6204 5.18827 19.8978 6.22354C20 6.60504 20 7.07003 20 8V17.2C20 18.8802 20 19.7202 19.673 20.362C19.3854 20.9265 18.9265 21.3854 18.362 21.673C17.7202 22 16.8802 22 15.2 22H8.8C7.11984 22 6.27976 22 5.63803 21.673C5.07354 21.3854 4.6146 20.9265 4.32698 20.362C4 19.7202 4 18.8802 4 17.2V8C4 7.07003 4 6.60504 4.10222 6.22354C4.37962 5.18827 5.18827 4.37962 6.22354 4.10222C6.60504 4 7.07003 4 8 4M9.6 6H14.4C14.9601 6 15.2401 6 15.454 5.89101C15.6422 5.79513 15.7951 5.64215 15.891 5.45399C16 5.24008 16 4.96005 16 4.4V3.6C16 3.03995 16 2.75992 15.891 2.54601C15.7951 2.35785 15.6422 2.20487 15.454 2.10899C15.2401 2 14.9601 2 14.4 2H9.6C9.03995 2 8.75992 2 8.54601 2.10899C8.35785 2.20487 8.20487 2.35785 8.10899 2.54601C8 2.75992 8 3.03995 8 3.6V4.4C8 4.96005 8 5.24008 8.10899 5.45399C8.20487 5.64215 8.35785 5.79513 8.54601 5.89101C8.75992 6 9.03995 6 9.6 6Z",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "Clipboard"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Clipboard.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 = 'Clipboard'
export default Icon

View File

@ -1 +1,2 @@
export { default as Clipboard } from './Clipboard'
export { default as FilePlus02 } from './FilePlus02' export { default as FilePlus02 } from './FilePlus02'

View File

@ -0,0 +1,29 @@
{
"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": "path",
"attributes": {
"d": "M5 7.8C5 6.11984 5 5.27976 5.32698 4.63803C5.6146 4.07354 6.07354 3.6146 6.63803 3.32698C7.27976 3 8.11984 3 9.8 3H14.2C15.8802 3 16.7202 3 17.362 3.32698C17.9265 3.6146 18.3854 4.07354 18.673 4.63803C19 5.27976 19 6.11984 19 7.8V21L12 17L5 21V7.8Z",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "Bookmark"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Bookmark.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 = 'Bookmark'
export default Icon

View File

@ -1,4 +1,5 @@
export { default as AtSign } from './AtSign' export { default as AtSign } from './AtSign'
export { default as Bookmark } from './Bookmark'
export { default as Check } from './Check' export { default as Check } from './Check'
export { default as DotsHorizontal } from './DotsHorizontal' export { default as DotsHorizontal } from './DotsHorizontal'
export { default as Edit03 } from './Edit03' export { default as Edit03 } from './Edit03'

View File

@ -0,0 +1,29 @@
{
"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": "path",
"attributes": {
"d": "M4.5 22V17M4.5 7V2M2 4.5H7M2 19.5H7M13 3L11.2658 7.50886C10.9838 8.24209 10.8428 8.60871 10.6235 8.91709C10.4292 9.1904 10.1904 9.42919 9.91709 9.62353C9.60871 9.8428 9.24209 9.98381 8.50886 10.2658L4 12L8.50886 13.7342C9.24209 14.0162 9.60871 14.1572 9.91709 14.3765C10.1904 14.5708 10.4292 14.8096 10.6235 15.0829C10.8428 15.3913 10.9838 15.7579 11.2658 16.4911L13 21L14.7342 16.4911C15.0162 15.7579 15.1572 15.3913 15.3765 15.0829C15.5708 14.8096 15.8096 14.5708 16.0829 14.3765C16.3913 14.1572 16.7579 14.0162 17.4911 13.7342L22 12L17.4911 10.2658C16.7579 9.98381 16.3913 9.8428 16.0829 9.62353C15.8096 9.42919 15.5708 9.1904 15.3765 8.91709C15.1572 8.60871 15.0162 8.24209 14.7342 7.50886L13 3Z",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "Stars02"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './Stars02.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 = 'Stars02'
export default Icon

View File

@ -0,0 +1 @@
export { default as Stars02 } from './Stars02'

View File

@ -3,11 +3,12 @@ import type { FC } from 'react'
import React, { useEffect, useRef, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import cn from 'classnames' import cn from 'classnames'
import { useBoolean, useClickAway, useGetState } from 'ahooks' import { useBoolean, useClickAway } from 'ahooks'
import { XMarkIcon } from '@heroicons/react/24/outline' import { XMarkIcon } from '@heroicons/react/24/outline'
import TabHeader from '../../base/tab-header' import TabHeader from '../../base/tab-header'
import Button from '../../base/button' import Button from '../../base/button'
import { checkOrSetAccessToken } from '../utils' import { checkOrSetAccessToken } from '../utils'
import { AlertCircle } from '../../base/icons/src/vender/solid/alertsAndFeedback'
import s from './style.module.css' import s from './style.module.css'
import RunBatch from './run-batch' import RunBatch from './run-batch'
import ResDownload from './run-batch/res-download' import ResDownload from './run-batch/res-download'
@ -25,12 +26,12 @@ import SavedItems from '@/app/components/app/text-generate/saved-items'
import type { InstalledApp } from '@/models/explore' import type { InstalledApp } from '@/models/explore'
import { DEFAULT_VALUE_MAX_LEN, appDefaultIconBackground } from '@/config' import { DEFAULT_VALUE_MAX_LEN, appDefaultIconBackground } from '@/config'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group.
const PARALLEL_LIMIT = 5
enum TaskStatus { enum TaskStatus {
pending = 'pending', pending = 'pending',
running = 'running', running = 'running',
completed = 'completed', completed = 'completed',
failed = 'failed',
} }
type TaskParam = { type TaskParam = {
@ -99,15 +100,41 @@ const TextGeneration: FC<IMainProps> = ({
showResSidebar() showResSidebar()
} }
const [allTaskList, setAllTaskList, getLatestTaskList] = useGetState<Task[]>([]) const [controlRetry, setControlRetry] = useState(0)
const handleRetryAllFailedTask = () => {
setControlRetry(Date.now())
}
const [allTaskList, doSetAllTaskList] = useState<Task[]>([])
const allTaskListRef = useRef<Task[]>([])
const getLatestTaskList = () => allTaskListRef.current
const setAllTaskList = (taskList: Task[]) => {
doSetAllTaskList(taskList)
allTaskListRef.current = taskList
}
const pendingTaskList = allTaskList.filter(task => task.status === TaskStatus.pending) const pendingTaskList = allTaskList.filter(task => task.status === TaskStatus.pending)
const noPendingTask = pendingTaskList.length === 0 const noPendingTask = pendingTaskList.length === 0
const showTaskList = allTaskList.filter(task => task.status !== TaskStatus.pending) const showTaskList = allTaskList.filter(task => task.status !== TaskStatus.pending)
const [currGroupNum, doSetCurrGroupNum] = useState(0)
const currGroupNumRef = useRef(0)
const setCurrGroupNum = (num: number) => {
doSetCurrGroupNum(num)
currGroupNumRef.current = num
}
const getCurrGroupNum = () => {
return currGroupNumRef.current
}
const allSuccessTaskList = allTaskList.filter(task => task.status === TaskStatus.completed)
const allFailedTaskList = allTaskList.filter(task => task.status === TaskStatus.failed)
const allTaskFinished = allTaskList.every(task => task.status === TaskStatus.completed) const allTaskFinished = allTaskList.every(task => task.status === TaskStatus.completed)
const [batchCompletionRes, setBatchCompletionRes, getBatchCompletionRes] = useGetState<Record<string, string>>({}) const allTaskRuned = allTaskList.every(task => [TaskStatus.completed, TaskStatus.failed].includes(task.status))
const [batchCompletionRes, doSetBatchCompletionRes] = useState<Record<string, string>>({})
const batchCompletionResRef = useRef<Record<string, string>>({})
const setBatchCompletionRes = (res: Record<string, string>) => {
doSetBatchCompletionRes(res)
batchCompletionResRef.current = res
}
const getBatchCompletionRes = () => batchCompletionResRef.current
const exportRes = allTaskList.map((task) => { const exportRes = allTaskList.map((task) => {
if (allTaskList.length > 0 && !allTaskFinished)
return {}
const batchCompletionResLatest = getBatchCompletionRes() const batchCompletionResLatest = getBatchCompletionRes()
const res: Record<string, string> = {} const res: Record<string, string> = {}
const { inputs } = task.params const { inputs } = task.params
@ -123,7 +150,6 @@ const TextGeneration: FC<IMainProps> = ({
return false return false
} }
const headerData = data[0] const headerData = data[0]
const varLen = promptConfig?.prompt_variables.length || 0
let isMapVarName = true let isMapVarName = true
promptConfig?.prompt_variables.forEach((item, index) => { promptConfig?.prompt_variables.forEach((item, index) => {
if (!isMapVarName) if (!isMapVarName)
@ -234,7 +260,7 @@ const TextGeneration: FC<IMainProps> = ({
} }
return { return {
id: i + 1, id: i + 1,
status: i < PARALLEL_LIMIT ? TaskStatus.running : TaskStatus.pending, status: i < GROUP_SIZE ? TaskStatus.running : TaskStatus.pending,
params: { params: {
inputs, inputs,
}, },
@ -248,20 +274,28 @@ const TextGeneration: FC<IMainProps> = ({
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
showResSidebar() showResSidebar()
} }
const handleCompleted = (completionRes: string, taskId?: number) => { const handleCompleted = (completionRes: string, taskId?: number, isSuccess?: boolean) => {
const allTasklistLatest = getLatestTaskList() const allTasklistLatest = getLatestTaskList()
const batchCompletionResLatest = getBatchCompletionRes() const batchCompletionResLatest = getBatchCompletionRes()
const pendingTaskList = allTasklistLatest.filter(task => task.status === TaskStatus.pending) const pendingTaskList = allTasklistLatest.filter(task => task.status === TaskStatus.pending)
const nextPendingTaskId = pendingTaskList[0]?.id const hadRunedTaskNum = 1 + allTasklistLatest.filter(task => [TaskStatus.completed, TaskStatus.failed].includes(task.status)).length
// console.log(`start: ${allTasklistLatest.map(item => item.status).join(',')}`) const needToAddNextGroupTask = (getCurrGroupNum() !== hadRunedTaskNum) && pendingTaskList.length > 0 && (hadRunedTaskNum % GROUP_SIZE === 0 || (allTasklistLatest.length - hadRunedTaskNum < GROUP_SIZE))
// avoid add many task at the same time
if (needToAddNextGroupTask)
setCurrGroupNum(hadRunedTaskNum)
// console.group()
// console.log(`[#${taskId}]: ${isSuccess ? 'success' : 'fail'}.currGroupNum: ${getCurrGroupNum()}.hadRunedTaskNum: ${hadRunedTaskNum}, needToAddNextGroupTask: ${needToAddNextGroupTask}`)
// console.log([...allTasklistLatest.filter(task => [TaskStatus.completed, TaskStatus.failed].includes(task.status)).map(item => item.id), taskId].sort((a: any, b: any) => a - b).join(','))
// console.groupEnd()
const nextPendingTaskIds = needToAddNextGroupTask ? pendingTaskList.slice(0, GROUP_SIZE).map(item => item.id) : []
const newAllTaskList = allTasklistLatest.map((item) => { const newAllTaskList = allTasklistLatest.map((item) => {
if (item.id === taskId) { if (item.id === taskId) {
return { return {
...item, ...item,
status: TaskStatus.completed, status: isSuccess ? TaskStatus.completed : TaskStatus.failed,
} }
} }
if (item.id === nextPendingTaskId) { if (needToAddNextGroupTask && nextPendingTaskIds.includes(item.id)) {
return { return {
...item, ...item,
status: TaskStatus.running, status: TaskStatus.running,
@ -269,7 +303,6 @@ const TextGeneration: FC<IMainProps> = ({
} }
return item return item
}) })
// console.log(`end: ${newAllTaskList.map(item => item.status).join(',')}`)
setAllTaskList(newAllTaskList) setAllTaskList(newAllTaskList)
if (taskId) { if (taskId) {
setBatchCompletionRes({ setBatchCompletionRes({
@ -333,10 +366,12 @@ const TextGeneration: FC<IMainProps> = ({
isMobile={isMobile} isMobile={isMobile}
isInstalledApp={!!isInstalledApp} isInstalledApp={!!isInstalledApp}
installedAppInfo={installedAppInfo} installedAppInfo={installedAppInfo}
isError={task?.status === TaskStatus.failed}
promptConfig={promptConfig} promptConfig={promptConfig}
moreLikeThisEnabled={!!moreLikeThisConfig?.enabled} moreLikeThisEnabled={!!moreLikeThisConfig?.enabled}
inputs={isCallBatchAPI ? (task as Task).params.inputs : inputs} inputs={isCallBatchAPI ? (task as Task).params.inputs : inputs}
controlSend={controlSend} controlSend={controlSend}
controlRetry={task?.status === TaskStatus.failed ? controlRetry : 0}
controlStopResponding={controlStopResponding} controlStopResponding={controlStopResponding}
onShowRes={showResSidebar} onShowRes={showResSidebar}
handleSaveMessage={handleSaveMessage} handleSaveMessage={handleSaveMessage}
@ -365,7 +400,19 @@ const TextGeneration: FC<IMainProps> = ({
<div className='text-lg text-gray-800 font-semibold'>{t('share.generation.title')}</div> <div className='text-lg text-gray-800 font-semibold'>{t('share.generation.title')}</div>
</div> </div>
<div className='flex items-center space-x-2'> <div className='flex items-center space-x-2'>
{allTaskList.length > 0 && allTaskFinished && ( {allFailedTaskList.length > 0 && (
<div className='flex items-center'>
<AlertCircle className='w-4 h-4 text-[#D92D20]' />
<div className='ml-1 text-[#D92D20]'>{t('share.generation.batchFailed.info', { num: allFailedTaskList.length })}</div>
<Button
type='primary'
className='ml-2 !h-8 !px-3'
onClick={handleRetryAllFailedTask}
>{t('share.generation.batchFailed.retry')}</Button>
<div className='mx-3 w-[1px] h-3.5 bg-gray-200'></div>
</div>
)}
{allSuccessTaskList.length > 0 && (
<ResDownload <ResDownload
isMobile={isMobile} isMobile={isMobile}
values={exportRes} values={exportRes}
@ -394,8 +441,12 @@ const TextGeneration: FC<IMainProps> = ({
</div> </div>
) )
if (!appId || !siteInfo || !promptConfig) if (!appId || !siteInfo || !promptConfig) {
return <Loading type='app' /> return (
<div className='flex items-center h-screen'>
<Loading type='app' />
</div>)
}
return ( return (
<> <>
@ -466,6 +517,7 @@ const TextGeneration: FC<IMainProps> = ({
<RunBatch <RunBatch
vars={promptConfig.prompt_variables} vars={promptConfig.prompt_variables}
onSend={handleRunBatch} onSend={handleRunBatch}
isAllFinished={allTaskRuned}
/> />
</div> </div>

View File

@ -1,7 +1,7 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useEffect, useState } from 'react' import React, { useEffect, useRef, useState } from 'react'
import { useBoolean, useGetState } from 'ahooks' import { useBoolean } from 'ahooks'
import { t } from 'i18next' import { t } from 'i18next'
import cn from 'classnames' import cn from 'classnames'
import TextGenerationRes from '@/app/components/app/text-generate/item' import TextGenerationRes from '@/app/components/app/text-generate/item'
@ -18,10 +18,12 @@ export type IResultProps = {
isMobile: boolean isMobile: boolean
isInstalledApp: boolean isInstalledApp: boolean
installedAppInfo?: InstalledApp installedAppInfo?: InstalledApp
isError: boolean
promptConfig: PromptConfig | null promptConfig: PromptConfig | null
moreLikeThisEnabled: boolean moreLikeThisEnabled: boolean
inputs: Record<string, any> inputs: Record<string, any>
controlSend?: number controlSend?: number
controlRetry?: number
controlStopResponding?: number controlStopResponding?: number
onShowRes: () => void onShowRes: () => void
handleSaveMessage: (messageId: string) => void handleSaveMessage: (messageId: string) => void
@ -35,10 +37,12 @@ const Result: FC<IResultProps> = ({
isMobile, isMobile,
isInstalledApp, isInstalledApp,
installedAppInfo, installedAppInfo,
isError,
promptConfig, promptConfig,
moreLikeThisEnabled, moreLikeThisEnabled,
inputs, inputs,
controlSend, controlSend,
controlRetry,
controlStopResponding, controlStopResponding,
onShowRes, onShowRes,
handleSaveMessage, handleSaveMessage,
@ -51,7 +55,13 @@ const Result: FC<IResultProps> = ({
setResponsingFalse() setResponsingFalse()
}, [controlStopResponding]) }, [controlStopResponding])
const [completionRes, setCompletionRes, getCompletionRes] = useGetState('') const [completionRes, doSetCompletionRes] = useState('')
const completionResRef = useRef('')
const setCompletionRes = (res: string) => {
completionResRef.current = res
doSetCompletionRes(res)
}
const getCompletionRes = () => completionResRef.current
const { notify } = Toast const { notify } = Toast
const isNoData = !completionRes const isNoData = !completionRes
@ -124,6 +134,17 @@ const Result: FC<IResultProps> = ({
onShowRes() onShowRes()
setResponsingTrue() setResponsingTrue()
const startTime = Date.now()
let isTimeout = false
const runId = setInterval(() => {
if (Date.now() - startTime > 1000 * 60) { // 1min timeout
clearInterval(runId)
setResponsingFalse()
onCompleted(getCompletionRes(), taskId, false)
isTimeout = true
console.log(`[#${taskId}]: timeout`)
}
}, 1000)
sendCompletionMessage(data, { sendCompletionMessage(data, {
onData: (data: string, _isFirstMessage: boolean, { messageId }) => { onData: (data: string, _isFirstMessage: boolean, { messageId }) => {
tempMessageId = messageId tempMessageId = messageId
@ -131,13 +152,21 @@ const Result: FC<IResultProps> = ({
setCompletionRes(res.join('')) setCompletionRes(res.join(''))
}, },
onCompleted: () => { onCompleted: () => {
if (isTimeout)
return
setResponsingFalse() setResponsingFalse()
setMessageId(tempMessageId) setMessageId(tempMessageId)
onCompleted(getCompletionRes(), taskId, true) onCompleted(getCompletionRes(), taskId, true)
clearInterval(runId)
}, },
onError() { onError() {
if (isTimeout)
return
setResponsingFalse() setResponsingFalse()
onCompleted(getCompletionRes(), taskId, false) onCompleted(getCompletionRes(), taskId, false)
clearInterval(runId)
}, },
}, isInstalledApp, installedAppInfo?.id) }, isInstalledApp, installedAppInfo?.id)
} }
@ -150,9 +179,16 @@ const Result: FC<IResultProps> = ({
} }
}, [controlSend]) }, [controlSend])
useEffect(() => {
if (controlRetry)
handleSend()
}, [controlRetry])
const renderTextGenerationRes = () => ( const renderTextGenerationRes = () => (
<TextGenerationRes <TextGenerationRes
className='mt-3' className='mt-3'
isError={isError}
onRetry={handleSend}
content={completionRes} content={completionRes}
messageId={messageId} messageId={messageId}
isInWebApp isInWebApp

View File

@ -5,18 +5,21 @@ import {
PlayIcon, PlayIcon,
} from '@heroicons/react/24/solid' } from '@heroicons/react/24/solid'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import CSVReader from './csv-reader' import CSVReader from './csv-reader'
import CSVDownload from './csv-download' import CSVDownload from './csv-download'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { Loading02 } from '@/app/components/base/icons/src/vender/line/general'
export type IRunBatchProps = { export type IRunBatchProps = {
vars: { name: string }[] vars: { name: string }[]
onSend: (data: string[][]) => void onSend: (data: string[][]) => void
isAllFinished: boolean
} }
const RunBatch: FC<IRunBatchProps> = ({ const RunBatch: FC<IRunBatchProps> = ({
vars, vars,
onSend, onSend,
isAllFinished,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -31,6 +34,7 @@ const RunBatch: FC<IRunBatchProps> = ({
const handleSend = () => { const handleSend = () => {
onSend(csvData) onSend(csvData)
} }
const Icon = isAllFinished ? PlayIcon : Loading02
return ( return (
<div className='pt-4'> <div className='pt-4'>
<CSVReader onParsed={handleParsed} /> <CSVReader onParsed={handleParsed} />
@ -41,9 +45,9 @@ const RunBatch: FC<IRunBatchProps> = ({
type="primary" type="primary"
className='mt-4 !h-8 !pl-3 !pr-4' className='mt-4 !h-8 !pl-3 !pr-4'
onClick={handleSend} onClick={handleSend}
disabled={!isParsed} disabled={!isParsed || !isAllFinished}
> >
<PlayIcon className="shrink-0 w-4 h-4 mr-1" aria-hidden="true" /> <Icon className={cn(!isAllFinished && 'animate-spin', 'shrink-0 w-4 h-4 mr-1')} aria-hidden="true" />
<span className='uppercase text-[13px]'>{t('share.generation.run')}</span> <span className='uppercase text-[13px]'>{t('share.generation.run')}</span>
</Button> </Button>
</div> </div>

View File

@ -54,6 +54,11 @@ 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',
batchFailed: {
info: '{{num}} failed executions',
retry: 'Retry',
outputPlaceholder: 'No output content',
},
errorMsg: { errorMsg: {
empty: 'Please input content in the uploaded file.', empty: 'Please input content in the uploaded file.',
fileStructNotMatch: 'The uploaded CSV file not match the struct.', fileStructNotMatch: 'The uploaded CSV file not match the struct.',

View File

@ -51,6 +51,11 @@ const translation = {
csvStructureTitle: 'CSV 文件必须符合以下结构:', csvStructureTitle: 'CSV 文件必须符合以下结构:',
downloadTemplate: '下载模板', downloadTemplate: '下载模板',
field: '', field: '',
batchFailed: {
info: '{{num}} 次运行失败',
retry: '重试',
outputPlaceholder: '无输出内容',
},
errorMsg: { errorMsg: {
empty: '上传文件的内容不能为空', empty: '上传文件的内容不能为空',
fileStructNotMatch: '上传文件的内容与结构不匹配', fileStructNotMatch: '上传文件的内容与结构不匹配',