mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 06:58:05 +08:00
merge
This commit is contained in:
@ -19,3 +19,6 @@ NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Disable Upload Image as WebApp icon default is false
|
||||
NEXT_PUBLIC_UPLOAD_IMAGE_AS_ICON=false
|
||||
|
||||
# The timeout for the text generation in millisecond
|
||||
NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=60000
|
||||
|
||||
@ -63,7 +63,6 @@ const AppPublisher = ({
|
||||
const [published, setPublished] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const [publishedTime, setPublishedTime] = useState<number | undefined>(publishedAt)
|
||||
const { app_base_url: appBaseURL = '', access_token: accessToken = '' } = appDetail?.site ?? {}
|
||||
const appMode = (appDetail?.mode !== 'completion' && appDetail?.mode !== 'workflow') ? 'chat' : appDetail.mode
|
||||
const appURL = `${appBaseURL}/${appMode}/${accessToken}`
|
||||
@ -77,7 +76,6 @@ const AppPublisher = ({
|
||||
try {
|
||||
await onPublish?.(modelAndParameter)
|
||||
setPublished(true)
|
||||
setPublishedTime(Date.now())
|
||||
}
|
||||
catch (e) {
|
||||
setPublished(false)
|
||||
@ -133,13 +131,13 @@ const AppPublisher = ({
|
||||
<div className='w-[336px] bg-white rounded-2xl border-[0.5px] border-gray-200 shadow-xl'>
|
||||
<div className='p-4 pt-3'>
|
||||
<div className='flex items-center h-6 text-xs font-medium text-gray-500 uppercase'>
|
||||
{publishedTime ? t('workflow.common.latestPublished') : t('workflow.common.currentDraftUnpublished')}
|
||||
{publishedAt ? t('workflow.common.latestPublished') : t('workflow.common.currentDraftUnpublished')}
|
||||
</div>
|
||||
{publishedTime
|
||||
{publishedAt
|
||||
? (
|
||||
<div className='flex justify-between items-center h-[18px]'>
|
||||
<div className='flex items-center mt-[3px] mb-[3px] leading-[18px] text-[13px] font-medium text-gray-700'>
|
||||
{t('workflow.common.publishedAt')} {formatTimeFromNow(publishedTime)}
|
||||
{t('workflow.common.publishedAt')} {formatTimeFromNow(publishedAt)}
|
||||
</div>
|
||||
<Button
|
||||
className={`
|
||||
@ -177,18 +175,18 @@ const AppPublisher = ({
|
||||
{
|
||||
published
|
||||
? t('workflow.common.published')
|
||||
: publishedTime ? t('workflow.common.update') : t('workflow.common.publish')
|
||||
: publishedAt ? t('workflow.common.update') : t('workflow.common.publish')
|
||||
}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='p-4 pt-3 border-t-[0.5px] border-t-black/5'>
|
||||
<SuggestedAction disabled={!publishedTime} link={appURL} icon={<PlayCircle />}>{t('workflow.common.runApp')}</SuggestedAction>
|
||||
<SuggestedAction disabled={!publishedAt} link={appURL} icon={<PlayCircle />}>{t('workflow.common.runApp')}</SuggestedAction>
|
||||
{appDetail?.mode === 'workflow'
|
||||
? (
|
||||
<SuggestedAction
|
||||
disabled={!publishedTime}
|
||||
disabled={!publishedAt}
|
||||
link={`${appURL}${appURL.includes('?') ? '&' : '?'}mode=batch`}
|
||||
icon={<LeftIndent02 className='w-4 h-4' />}
|
||||
>
|
||||
@ -201,16 +199,16 @@ const AppPublisher = ({
|
||||
setEmbeddingModalOpen(true)
|
||||
handleTrigger()
|
||||
}}
|
||||
disabled={!publishedTime}
|
||||
disabled={!publishedAt}
|
||||
icon={<CodeBrowser className='w-4 h-4' />}
|
||||
>
|
||||
{t('workflow.common.embedIntoSite')}
|
||||
</SuggestedAction>
|
||||
)}
|
||||
<SuggestedAction disabled={!publishedTime} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction>
|
||||
<SuggestedAction disabled={!publishedAt} link='./develop' icon={<FileText className='w-4 h-4' />}>{t('workflow.common.accessAPIReference')}</SuggestedAction>
|
||||
{appDetail?.mode === 'workflow' && (
|
||||
<WorkflowToolConfigureButton
|
||||
disabled={!publishedTime}
|
||||
disabled={!publishedAt}
|
||||
published={!!toolPublished}
|
||||
detailNeedUpdate={!!toolPublished && published}
|
||||
workflowAppId={appDetail?.id}
|
||||
|
||||
@ -337,7 +337,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
|
||||
return (
|
||||
<div ref={ref} className='rounded-xl border-[0.5px] border-gray-200 h-full flex flex-col overflow-auto'>
|
||||
{/* Panel Header */}
|
||||
<div className='border-b border-gray-100 py-4 px-6 flex items-center justify-between'>
|
||||
<div className='border-b border-gray-100 py-4 px-6 flex items-center justify-between bg-components-panel-bg'>
|
||||
<div>
|
||||
<div className='text-gray-500 text-[10px] leading-[14px]'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</div>
|
||||
{isChatMode && (
|
||||
@ -730,7 +730,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
|
||||
onClose={onCloseDrawer}
|
||||
mask={isMobile}
|
||||
footer={null}
|
||||
panelClassname='mt-16 mx-2 sm:mr-2 mb-4 !p-0 !max-w-[640px] rounded-xl'
|
||||
panelClassname='mt-16 mx-2 sm:mr-2 mb-4 !p-0 !max-w-[640px] rounded-xl bg-background-gradient-bg-fill-chat-bg-1'
|
||||
>
|
||||
<DrawerContext.Provider value={{
|
||||
onClose: onCloseDrawer,
|
||||
|
||||
@ -8,20 +8,19 @@ import type {
|
||||
ChatConfig,
|
||||
ChatItem,
|
||||
} from '../../types'
|
||||
import { useChatContext } from '../context'
|
||||
import Operation from './operation'
|
||||
import AgentContent from './agent-content'
|
||||
import BasicContent from './basic-content'
|
||||
import SuggestedQuestions from './suggested-questions'
|
||||
import More from './more'
|
||||
import WorkflowProcess from './workflow-process'
|
||||
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import { AnswerTriangle } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
|
||||
import Citation from '@/app/components/base/chat/chat/citation'
|
||||
import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
|
||||
import type { AppData } from '@/models/share'
|
||||
import cn from '@/utils/classnames'
|
||||
import AnswerIcon from '@/app/components/base/answer-icon'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type AnswerProps = {
|
||||
item: ChatItem
|
||||
@ -58,26 +57,24 @@ const Answer: FC<AnswerProps> = ({
|
||||
} = item
|
||||
const hasAgentThoughts = !!agent_thoughts?.length
|
||||
|
||||
const [containerWidth] = useState(0)
|
||||
const [containerWidth, setContainerWidth] = useState(0)
|
||||
const [contentWidth, setContentWidth] = useState(0)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const {
|
||||
config: chatContextConfig,
|
||||
} = useChatContext()
|
||||
const getContainerWidth = () => {
|
||||
if (containerRef.current)
|
||||
setContainerWidth(containerRef.current?.clientWidth + 16)
|
||||
}
|
||||
useEffect(() => {
|
||||
getContainerWidth()
|
||||
}, [])
|
||||
|
||||
const voiceRef = useRef(chatContextConfig?.text_to_speech?.voice)
|
||||
const getContentWidth = () => {
|
||||
if (contentRef.current)
|
||||
setContentWidth(contentRef.current?.clientWidth)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
voiceRef.current = chatContextConfig?.text_to_speech?.voice
|
||||
}
|
||||
, [chatContextConfig?.text_to_speech?.voice])
|
||||
|
||||
useEffect(() => {
|
||||
if (!responding)
|
||||
getContentWidth()
|
||||
@ -86,35 +83,20 @@ const Answer: FC<AnswerProps> = ({
|
||||
return (
|
||||
<div className='flex mb-2 last:mb-0'>
|
||||
<div className='shrink-0 relative w-10 h-10'>
|
||||
{
|
||||
answerIcon || <AnswerIcon />
|
||||
}
|
||||
{
|
||||
responding && (
|
||||
<div className='absolute -top-[3px] -left-[3px] pl-[6px] flex items-center w-4 h-4 bg-white rounded-full shadow-xs border-[0.5px] border-gray-50'>
|
||||
<LoadingAnim type='avatar' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{answerIcon || <AnswerIcon />}
|
||||
{responding && (
|
||||
<div className='absolute -top-[3px] -left-[3px] pl-[6px] flex items-center w-4 h-4 bg-white rounded-full shadow-xs border-[0.5px] border-gray-50'>
|
||||
<LoadingAnim type='avatar' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className='chat-answer-container group grow w-0 ml-4' ref={containerRef}>
|
||||
<div className={`group relative pr-10 ${chatAnswerContainerInner}`}>
|
||||
<div className={cn('group relative pr-10', chatAnswerContainerInner)}>
|
||||
<AnswerTriangle className='absolute -left-2 top-0 w-2 h-3 text-gray-100' />
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={cn(
|
||||
'relative inline-block p-2 max-w-full bg-chat-bubble-bg rounded-2xl border-t border-t-divider-subtle text-sm text-gray-900',
|
||||
workflowProcess && 'w-full',
|
||||
)}
|
||||
className={cn('relative inline-block px-4 py-3 max-w-full bg-gray-100 rounded-b-2xl rounded-tr-2xl text-sm text-gray-900', workflowProcess && 'w-full')}
|
||||
>
|
||||
{annotation?.id && (
|
||||
<div
|
||||
className='absolute -top-3.5 -right-3.5 box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7] shadow-md group-hover:hidden'
|
||||
>
|
||||
<div className='p-1 rounded-lg bg-[#EEF4FF] '>
|
||||
<MessageFast className='w-4 h-4' />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
!responding && (
|
||||
<Operation
|
||||
|
||||
@ -149,7 +149,7 @@ const Operation: FC<OperationProps> = ({
|
||||
/>
|
||||
)}
|
||||
{
|
||||
!positionRight && annotation?.id && (
|
||||
annotation?.id && (
|
||||
<div
|
||||
className='relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7] shadow-md group-hover:hidden'
|
||||
>
|
||||
|
||||
@ -117,9 +117,9 @@ const Chat: FC<ChatProps> = ({
|
||||
const userScrolledRef = useRef(false)
|
||||
|
||||
const handleScrollToBottom = useCallback(() => {
|
||||
if (chatContainerRef.current && !userScrolledRef.current)
|
||||
if (chatList.length > 1 && chatContainerRef.current && !userScrolledRef.current)
|
||||
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight
|
||||
}, [])
|
||||
}, [chatList.length])
|
||||
|
||||
const handleWindowResize = useCallback(() => {
|
||||
if (chatContainerRef.current)
|
||||
|
||||
@ -73,7 +73,7 @@ const CacheCtrlBtn: FC<Props> = ({
|
||||
setShowModal(false)
|
||||
}
|
||||
return (
|
||||
<div className={cn(className, 'inline-block')}>
|
||||
<div className={cn('inline-block', className)}>
|
||||
<div className='inline-flex p-0.5 space-x-0.5 rounded-lg bg-white border border-gray-100 shadow-md text-gray-500 cursor-pointer'>
|
||||
{cached
|
||||
? (
|
||||
@ -101,7 +101,6 @@ const CacheCtrlBtn: FC<Props> = ({
|
||||
? (
|
||||
<Tooltip
|
||||
popupContent={t('appDebug.feature.annotation.add')}
|
||||
needsDelay
|
||||
>
|
||||
<div
|
||||
className='p-1 rounded-md hover:bg-[#EEF4FF] hover:text-[#444CE7] cursor-pointer'
|
||||
@ -115,7 +114,6 @@ const CacheCtrlBtn: FC<Props> = ({
|
||||
}
|
||||
<Tooltip
|
||||
popupContent={t('appDebug.feature.annotation.edit')}
|
||||
needsDelay
|
||||
>
|
||||
<div
|
||||
className='p-1 cursor-pointer rounded-md hover:bg-black/5'
|
||||
|
||||
@ -1,26 +1,42 @@
|
||||
import type { FC } from 'react'
|
||||
import { useRef } from 'react'
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { t } from 'i18next'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { RiCloseLine, RiExternalLinkLine } from '@remixicon/react'
|
||||
import { RiAddBoxLine, RiCloseLine, RiDownloadCloud2Line, RiFileCopyLine, RiZoomInLine, RiZoomOutLine } from '@remixicon/react'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { randomString } from '@/utils'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
type ImagePreviewProps = {
|
||||
url: string
|
||||
title: string
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const isBase64 = (str: string): boolean => {
|
||||
try {
|
||||
return btoa(atob(str)) === str
|
||||
}
|
||||
catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const ImagePreview: FC<ImagePreviewProps> = ({
|
||||
url,
|
||||
title,
|
||||
onCancel,
|
||||
}) => {
|
||||
const selector = useRef(`copy-tooltip-${randomString(4)}`)
|
||||
const [scale, setScale] = useState(1)
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 })
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const imgRef = useRef<HTMLImageElement>(null)
|
||||
const dragStartRef = useRef({ x: 0, y: 0 })
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const openInNewTab = () => {
|
||||
// Open in a new window, considering the case when the page is inside an iframe
|
||||
if (url.startsWith('http')) {
|
||||
if (url.startsWith('http') || url.startsWith('https')) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
else if (url.startsWith('data:image')) {
|
||||
@ -29,34 +45,224 @@ const ImagePreview: FC<ImagePreviewProps> = ({
|
||||
win?.document.write(`<img src="${url}" alt="${title}" />`)
|
||||
}
|
||||
else {
|
||||
console.error('Unable to open image', url)
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: `Unable to open image: ${url}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
const downloadImage = () => {
|
||||
// Open in a new window, considering the case when the page is inside an iframe
|
||||
if (url.startsWith('http') || url.startsWith('https')) {
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = title
|
||||
a.click()
|
||||
}
|
||||
else if (url.startsWith('data:image')) {
|
||||
// Base64 image
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = title
|
||||
a.click()
|
||||
}
|
||||
else {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: `Unable to open image: ${url}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const zoomIn = () => {
|
||||
setScale(prevScale => Math.min(prevScale * 1.2, 15))
|
||||
}
|
||||
|
||||
const zoomOut = () => {
|
||||
setScale((prevScale) => {
|
||||
const newScale = Math.max(prevScale / 1.2, 0.5)
|
||||
if (newScale === 1)
|
||||
setPosition({ x: 0, y: 0 }) // Reset position when fully zoomed out
|
||||
|
||||
return newScale
|
||||
})
|
||||
}
|
||||
|
||||
const imageTobase64ToBlob = (base64: string, type = 'image/png'): Blob => {
|
||||
const byteCharacters = atob(base64)
|
||||
const byteArrays = []
|
||||
|
||||
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
|
||||
const slice = byteCharacters.slice(offset, offset + 512)
|
||||
const byteNumbers = new Array(slice.length)
|
||||
for (let i = 0; i < slice.length; i++)
|
||||
byteNumbers[i] = slice.charCodeAt(i)
|
||||
|
||||
const byteArray = new Uint8Array(byteNumbers)
|
||||
byteArrays.push(byteArray)
|
||||
}
|
||||
|
||||
return new Blob(byteArrays, { type })
|
||||
}
|
||||
|
||||
const imageCopy = useCallback(() => {
|
||||
const shareImage = async () => {
|
||||
try {
|
||||
const base64Data = url.split(',')[1]
|
||||
const blob = imageTobase64ToBlob(base64Data, 'image/png')
|
||||
|
||||
await navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
[blob.type]: blob,
|
||||
}),
|
||||
])
|
||||
setIsCopied(true)
|
||||
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.operation.imageCopied'),
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Failed to copy image:', err)
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${title}.png`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
|
||||
Toast.notify({
|
||||
type: 'info',
|
||||
message: t('common.operation.imageDownloaded'),
|
||||
})
|
||||
}
|
||||
}
|
||||
shareImage()
|
||||
}, [title, url])
|
||||
|
||||
const handleWheel = useCallback((e: React.WheelEvent<HTMLDivElement>) => {
|
||||
if (e.deltaY < 0)
|
||||
zoomIn()
|
||||
else
|
||||
zoomOut()
|
||||
}, [])
|
||||
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (scale > 1) {
|
||||
setIsDragging(true)
|
||||
dragStartRef.current = { x: e.clientX - position.x, y: e.clientY - position.y }
|
||||
}
|
||||
}, [scale, position])
|
||||
|
||||
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (isDragging && scale > 1) {
|
||||
const deltaX = e.clientX - dragStartRef.current.x
|
||||
const deltaY = e.clientY - dragStartRef.current.y
|
||||
|
||||
// Calculate boundaries
|
||||
const imgRect = imgRef.current?.getBoundingClientRect()
|
||||
const containerRect = imgRef.current?.parentElement?.getBoundingClientRect()
|
||||
|
||||
if (imgRect && containerRect) {
|
||||
const maxX = (imgRect.width * scale - containerRect.width) / 2
|
||||
const maxY = (imgRect.height * scale - containerRect.height) / 2
|
||||
|
||||
setPosition({
|
||||
x: Math.max(-maxX, Math.min(maxX, deltaX)),
|
||||
y: Math.max(-maxY, Math.min(maxY, deltaY)),
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [isDragging, scale])
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setIsDragging(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
return () => {
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
}, [handleMouseUp])
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape')
|
||||
onCancel()
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
|
||||
// Set focus to the container element
|
||||
if (containerRef.current)
|
||||
containerRef.current.focus()
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [onCancel])
|
||||
|
||||
return createPortal(
|
||||
<div className='fixed inset-0 p-8 flex items-center justify-center bg-black/80 z-[1000]' onClick={e => e.stopPropagation()}>
|
||||
<div className='fixed inset-0 p-8 flex items-center justify-center bg-black/80 z-[1000] image-preview-container'
|
||||
onClick={e => e.stopPropagation()}
|
||||
onWheel={handleWheel}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
style={{ cursor: scale > 1 ? 'move' : 'default' }}
|
||||
tabIndex={-1}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
ref={imgRef}
|
||||
alt={title}
|
||||
src={url}
|
||||
src={isBase64(url) ? `data:image/png;base64,${url}` : url}
|
||||
className='max-w-full max-h-full'
|
||||
style={{
|
||||
transform: `scale(${scale}) translate(${position.x}px, ${position.y}px)`,
|
||||
transition: isDragging ? 'none' : 'transform 0.2s ease-in-out',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className='absolute top-6 right-6 flex items-center justify-center w-8 h-8 bg-white/8 rounded-lg backdrop-blur-[2px] cursor-pointer'
|
||||
onClick={onCancel}
|
||||
>
|
||||
<RiCloseLine className='w-4 h-4 text-white' />
|
||||
</div>
|
||||
<Tooltip
|
||||
selector={selector.current}
|
||||
content={(t('common.operation.openInNewTab') ?? 'Open in new tab')}
|
||||
className='z-10'
|
||||
>
|
||||
<Tooltip popupContent={t('common.operation.copyImage')}>
|
||||
<div className='absolute top-6 right-48 flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer'
|
||||
onClick={imageCopy}>
|
||||
{isCopied
|
||||
? <RiFileCopyLine className='w-4 h-4 text-green-500'/>
|
||||
: <RiFileCopyLine className='w-4 h-4 text-gray-500'/>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip popupContent={t('common.operation.zoomOut')}>
|
||||
<div className='absolute top-6 right-40 flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer'
|
||||
onClick={zoomOut}>
|
||||
<RiZoomOutLine className='w-4 h-4 text-gray-500'/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip popupContent={t('common.operation.zoomIn')}>
|
||||
<div className='absolute top-6 right-32 flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer'
|
||||
onClick={zoomIn}>
|
||||
<RiZoomInLine className='w-4 h-4 text-gray-500'/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip popupContent={t('common.operation.download')}>
|
||||
<div className='absolute top-6 right-24 flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer'
|
||||
onClick={downloadImage}>
|
||||
<RiDownloadCloud2Line className='w-4 h-4 text-gray-500'/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip popupContent={t('common.operation.openInNewTab')}>
|
||||
<div className='absolute top-6 right-16 flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer'
|
||||
onClick={openInNewTab}>
|
||||
<RiAddBoxLine className='w-4 h-4 text-gray-500'/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip popupContent={t('common.operation.close')}>
|
||||
<div
|
||||
className='absolute top-6 right-16 flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer'
|
||||
onClick={openInNewTab}
|
||||
>
|
||||
<RiExternalLinkLine className='w-4 h-4 text-white' />
|
||||
className='absolute top-6 right-6 flex items-center justify-center w-8 h-8 bg-white/8 rounded-lg backdrop-blur-[2px] cursor-pointer'
|
||||
onClick={onCancel}>
|
||||
<RiCloseLine className='w-4 h-4 text-gray-500'/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>,
|
||||
|
||||
@ -5,6 +5,7 @@ import RemarkMath from 'remark-math'
|
||||
import RemarkBreaks from 'remark-breaks'
|
||||
import RehypeKatex from 'rehype-katex'
|
||||
import RemarkGfm from 'remark-gfm'
|
||||
import RehypeRaw from 'rehype-raw'
|
||||
import SyntaxHighlighter from 'react-syntax-highlighter'
|
||||
import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs'
|
||||
import type { RefObject } from 'react'
|
||||
@ -18,6 +19,7 @@ import ImageGallery from '@/app/components/base/image-gallery'
|
||||
import { useChatContext } from '@/app/components/base/chat/chat/context'
|
||||
import VideoGallery from '@/app/components/base/video-gallery'
|
||||
import AudioGallery from '@/app/components/base/audio-gallery'
|
||||
import SVGRenderer from '@/app/components/base/svg-gallery'
|
||||
|
||||
// Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
|
||||
const capitalizationLanguageNameMap: Record<string, string> = {
|
||||
@ -40,6 +42,7 @@ const capitalizationLanguageNameMap: Record<string, string> = {
|
||||
powershell: 'PowerShell',
|
||||
json: 'JSON',
|
||||
latex: 'Latex',
|
||||
svg: 'SVG',
|
||||
}
|
||||
const getCorrectCapitalizationLanguageName = (language: string) => {
|
||||
if (!language)
|
||||
@ -107,6 +110,7 @@ const useLazyLoad = (ref: RefObject<Element>): boolean => {
|
||||
// Error: Minified React error 185;
|
||||
// visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message
|
||||
// or use the non-minified dev environment for full errors and additional helpful warnings.
|
||||
|
||||
const CodeBlock: CodeComponent = memo(({ inline, className, children, ...props }) => {
|
||||
const [isSVG, setIsSVG] = useState(true)
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
@ -134,7 +138,7 @@ const CodeBlock: CodeComponent = memo(({ inline, className, children, ...props }
|
||||
>
|
||||
<div className='text-[13px] text-gray-500 font-normal'>{languageShowName}</div>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{language === 'mermaid' && <SVGBtn isSVG={isSVG} setIsSVG={setIsSVG} />}
|
||||
{language === 'mermaid' && <SVGBtn isSVG={isSVG} setIsSVG={setIsSVG}/>}
|
||||
<CopyBtn
|
||||
className='mr-1'
|
||||
value={String(children).replace(/\n$/, '')}
|
||||
@ -144,12 +148,10 @@ const CodeBlock: CodeComponent = memo(({ inline, className, children, ...props }
|
||||
</div>
|
||||
{(language === 'mermaid' && isSVG)
|
||||
? (<Flowchart PrimitiveCode={String(children).replace(/\n$/, '')} />)
|
||||
: (
|
||||
(language === 'echarts')
|
||||
? (<div style={{ minHeight: '250px', minWidth: '250px' }}><ErrorBoundary><ReactEcharts
|
||||
option={chartData}
|
||||
>
|
||||
</ReactEcharts></ErrorBoundary></div>)
|
||||
: (language === 'echarts'
|
||||
? (<div style={{ minHeight: '350px', minWidth: '700px' }}><ErrorBoundary><ReactEcharts option={chartData} /></ErrorBoundary></div>)
|
||||
: (language === 'svg'
|
||||
? (<ErrorBoundary><SVGRenderer content={String(children).replace(/\n$/, '')} /></ErrorBoundary>)
|
||||
: (<SyntaxHighlighter
|
||||
{...props}
|
||||
style={atelierHeathLight}
|
||||
@ -162,17 +164,12 @@ const CodeBlock: CodeComponent = memo(({ inline, className, children, ...props }
|
||||
PreTag="div"
|
||||
>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</SyntaxHighlighter>))}
|
||||
</SyntaxHighlighter>)))}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<code {...props} className={className}>
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
: (<code {...props} className={className}>{children}</code>)
|
||||
}, [chartData, children, className, inline, isSVG, language, languageShowName, match, props])
|
||||
})
|
||||
|
||||
CodeBlock.displayName = 'CodeBlock'
|
||||
|
||||
const VideoBlock: CodeComponent = memo(({ node }) => {
|
||||
@ -230,6 +227,7 @@ export function Markdown(props: { content: string; className?: string }) {
|
||||
remarkPlugins={[[RemarkGfm, RemarkMath, { singleDollarTextMath: false }], RemarkBreaks]}
|
||||
rehypePlugins={[
|
||||
RehypeKatex,
|
||||
RehypeRaw as any,
|
||||
// The Rehype plug-in is used to remove the ref attribute of an element
|
||||
() => {
|
||||
return (tree) => {
|
||||
@ -244,6 +242,7 @@ export function Markdown(props: { content: string; className?: string }) {
|
||||
}
|
||||
},
|
||||
]}
|
||||
disallowedElements={['script', 'iframe', 'head', 'html', 'meta', 'link', 'style', 'body']}
|
||||
components={{
|
||||
code: CodeBlock,
|
||||
img: Img,
|
||||
@ -266,19 +265,23 @@ export function Markdown(props: { content: string; className?: string }) {
|
||||
// This can happen when a component attempts to access an undefined object that references an unregistered map, causing the program to crash.
|
||||
|
||||
export default class ErrorBoundary extends Component {
|
||||
constructor(props) {
|
||||
constructor(props: any) {
|
||||
super(props)
|
||||
this.state = { hasError: false }
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
componentDidCatch(error: any, errorInfo: any) {
|
||||
this.setState({ hasError: true })
|
||||
console.error(error, errorInfo)
|
||||
}
|
||||
|
||||
render() {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
if (this.state.hasError)
|
||||
return <div>Oops! ECharts reported a runtime error. <br />(see the browser console for more information)</div>
|
||||
return <div>Oops! An error occurred. This could be due to an ECharts runtime error or invalid SVG content. <br />(see the browser console for more information)</div>
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
79
web/app/components/base/svg-gallery/index.tsx
Normal file
79
web/app/components/base/svg-gallery/index.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { SVG } from '@svgdotjs/svg.js'
|
||||
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
|
||||
|
||||
export const SVGRenderer = ({ content }: { content: string }) => {
|
||||
const svgRef = useRef<HTMLDivElement>(null)
|
||||
const [imagePreview, setImagePreview] = useState('')
|
||||
const [windowSize, setWindowSize] = useState({
|
||||
width: typeof window !== 'undefined' ? window.innerWidth : 0,
|
||||
height: typeof window !== 'undefined' ? window.innerHeight : 0,
|
||||
})
|
||||
|
||||
const svgToDataURL = (svgElement: Element): string => {
|
||||
const svgString = new XMLSerializer().serializeToString(svgElement)
|
||||
const base64String = Buffer.from(svgString).toString('base64')
|
||||
return `data:image/svg+xml;base64,${base64String}`
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setWindowSize({ width: window.innerWidth, height: window.innerHeight })
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (svgRef.current) {
|
||||
try {
|
||||
svgRef.current.innerHTML = ''
|
||||
const draw = SVG().addTo(svgRef.current).size('100%', '100%')
|
||||
|
||||
const parser = new DOMParser()
|
||||
const svgDoc = parser.parseFromString(content, 'image/svg+xml')
|
||||
const svgElement = svgDoc.documentElement
|
||||
|
||||
if (!(svgElement instanceof SVGElement))
|
||||
throw new Error('Invalid SVG content')
|
||||
|
||||
const originalWidth = parseInt(svgElement.getAttribute('width') || '400', 10)
|
||||
const originalHeight = parseInt(svgElement.getAttribute('height') || '600', 10)
|
||||
const scale = Math.min(windowSize.width / originalWidth, windowSize.height / originalHeight, 1)
|
||||
const scaledWidth = originalWidth * scale
|
||||
const scaledHeight = originalHeight * scale
|
||||
draw.size(scaledWidth, scaledHeight)
|
||||
|
||||
const rootElement = draw.svg(content)
|
||||
rootElement.scale(scale)
|
||||
|
||||
rootElement.click(() => {
|
||||
setImagePreview(svgToDataURL(svgElement as Element))
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
if (svgRef.current)
|
||||
svgRef.current.innerHTML = 'Error rendering SVG. Wait for the image content to complete.'
|
||||
}
|
||||
}
|
||||
}, [content, windowSize])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={svgRef} style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
minHeight: '300px',
|
||||
maxHeight: '80vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
}} />
|
||||
{imagePreview && (<ImagePreview url={imagePreview} title='Preview' onCancel={() => setImagePreview('')} />)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SVGRenderer
|
||||
@ -413,3 +413,109 @@ Workflow applications offers non-session support and is ideal for translation, a
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/workflows/logs'
|
||||
method='GET'
|
||||
title='Get workflow logs'
|
||||
name='#Get-Workflow-Logs'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
Returns worklfow logs, with the first page returning the latest `{limit}` messages, i.e., in reverse order.
|
||||
|
||||
### Query
|
||||
|
||||
<Properties>
|
||||
<Property name='keyword' type='string' key='keyword'>
|
||||
Keyword to search
|
||||
</Property>
|
||||
<Property name='status' type='string' key='status'>
|
||||
succeeded/failed/stopped
|
||||
</Property>
|
||||
<Property name='page' type='int' key='page'>
|
||||
current page, default is 1.
|
||||
</Property>
|
||||
<Property name='limit' type='int' key='limit'>
|
||||
How many chat history messages to return in one request, default is 20.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### Response
|
||||
- `page` (int) Current page
|
||||
- `limit` (int) Number of returned items, if input exceeds system limit, returns system limit amount
|
||||
- `total` (int) Number of total items
|
||||
- `has_more` (bool) Whether there is a next page
|
||||
- `data` (array[object]) Log list
|
||||
- `id` (string) ID
|
||||
- `workflow_run` (object) Workflow run
|
||||
- `id` (string) ID
|
||||
- `version` (string) Version
|
||||
- `status` (string) status of execution, `running` / `succeeded` / `failed` / `stopped`
|
||||
- `error` (string) Optional reason of error
|
||||
- `elapsed_time` (float) total seconds to be used
|
||||
- `total_tokens` (int) tokens to be used
|
||||
- `total_steps` (int) default 0
|
||||
- `created_at` (timestamp) start time
|
||||
- `finished_at` (timestamp) end time
|
||||
- `created_from` (string) Created from
|
||||
- `created_by_role` (string) Created by role
|
||||
- `created_by_account` (string) Optional Created by account
|
||||
- `created_by_end_user` (object) Created by end user
|
||||
- `id` (string) ID
|
||||
- `type` (string) Type
|
||||
- `is_anonymous` (bool) Is anonymous
|
||||
- `session_id` (string) Session ID
|
||||
- `created_at` (timestamp) create time
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/workflows/logs" targetCode={`curl -X GET '${props.appDetail.api_base_url}/workflows/logs'\\\n --header 'Authorization: Bearer {api_key}'`}>
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X GET '${props.appDetail.api_base_url}/workflows/logs?limit=1'
|
||||
--header 'Authorization: Bearer {api_key}'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
### Response Example
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"page": 1,
|
||||
"limit": 1,
|
||||
"total": 7,
|
||||
"has_more": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "e41b93f1-7ca2-40fd-b3a8-999aeb499cc0",
|
||||
"workflow_run": {
|
||||
"id": "c0640fc8-03ef-4481-a96c-8a13b732a36e",
|
||||
"version": "2024-08-01 12:17:09.771832",
|
||||
"status": "succeeded",
|
||||
"error": null,
|
||||
"elapsed_time": 1.3588523610014818,
|
||||
"total_tokens": 0,
|
||||
"total_steps": 3,
|
||||
"created_at": 1726139643,
|
||||
"finished_at": 1726139644
|
||||
},
|
||||
"created_from": "service-api",
|
||||
"created_by_role": "end_user",
|
||||
"created_by_account": null,
|
||||
"created_by_end_user": {
|
||||
"id": "7f7d9117-dd9d-441d-8970-87e5e7e687a3",
|
||||
"type": "service_api",
|
||||
"is_anonymous": false,
|
||||
"session_id": "abc-123"
|
||||
},
|
||||
"created_at": 1726139644
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@ -409,3 +409,109 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
---
|
||||
|
||||
<Heading
|
||||
url='/workflows/logs'
|
||||
method='GET'
|
||||
title='获取 workflow 日志'
|
||||
name='#Get-Workflow-Logs'
|
||||
/>
|
||||
<Row>
|
||||
<Col>
|
||||
倒序返回workflow日志
|
||||
|
||||
### Query
|
||||
|
||||
<Properties>
|
||||
<Property name='keyword' type='string' key='keyword'>
|
||||
关键字
|
||||
</Property>
|
||||
<Property name='status' type='string' key='status'>
|
||||
执行状态 succeeded/failed/stopped
|
||||
</Property>
|
||||
<Property name='page' type='int' key='page'>
|
||||
当前页码, 默认1.
|
||||
</Property>
|
||||
<Property name='limit' type='int' key='limit'>
|
||||
每页条数, 默认20.
|
||||
</Property>
|
||||
</Properties>
|
||||
|
||||
### Response
|
||||
- `page` (int) 当前页码
|
||||
- `limit` (int) 每页条数
|
||||
- `total` (int) 总条数
|
||||
- `has_more` (bool) 是否还有更多数据
|
||||
- `data` (array[object]) 当前页码的数据
|
||||
- `id` (string) 标识
|
||||
- `workflow_run` (object) Workflow 执行日志
|
||||
- `id` (string) 标识
|
||||
- `version` (string) 版本
|
||||
- `status` (string) 执行状态, `running` / `succeeded` / `failed` / `stopped`
|
||||
- `error` (string) (可选) 错误
|
||||
- `elapsed_time` (float) 耗时,单位秒
|
||||
- `total_tokens` (int) 消耗的token数量
|
||||
- `total_steps` (int) 执行步骤长度
|
||||
- `created_at` (timestamp) 开始时间
|
||||
- `finished_at` (timestamp) 结束时间
|
||||
- `created_from` (string) 来源
|
||||
- `created_by_role` (string) 角色
|
||||
- `created_by_account` (string) (可选) 帐号
|
||||
- `created_by_end_user` (object) 用户
|
||||
- `id` (string) 标识
|
||||
- `type` (string) 类型
|
||||
- `is_anonymous` (bool) 是否匿名
|
||||
- `session_id` (string) 会话标识
|
||||
- `created_at` (timestamp) 创建时间
|
||||
</Col>
|
||||
<Col sticky>
|
||||
|
||||
<CodeGroup title="Request" tag="GET" label="/workflows/logs" targetCode={`curl -X GET '${props.appDetail.api_base_url}/workflows/logs'\\\n --header 'Authorization: Bearer {api_key}'`}>
|
||||
|
||||
```bash {{ title: 'cURL' }}
|
||||
curl -X GET '${props.appDetail.api_base_url}/workflows/logs?limit=1'
|
||||
--header 'Authorization: Bearer {api_key}'
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
### Response Example
|
||||
<CodeGroup title="Response">
|
||||
```json {{ title: 'Response' }}
|
||||
{
|
||||
"page": 1,
|
||||
"limit": 1,
|
||||
"total": 7,
|
||||
"has_more": true,
|
||||
"data": [
|
||||
{
|
||||
"id": "e41b93f1-7ca2-40fd-b3a8-999aeb499cc0",
|
||||
"workflow_run": {
|
||||
"id": "c0640fc8-03ef-4481-a96c-8a13b732a36e",
|
||||
"version": "2024-08-01 12:17:09.771832",
|
||||
"status": "succeeded",
|
||||
"error": null,
|
||||
"elapsed_time": 1.3588523610014818,
|
||||
"total_tokens": 0,
|
||||
"total_steps": 3,
|
||||
"created_at": 1726139643,
|
||||
"finished_at": 1726139644
|
||||
},
|
||||
"created_from": "service-api",
|
||||
"created_by_role": "end_user",
|
||||
"created_by_account": null,
|
||||
"created_by_end_user": {
|
||||
"id": "7f7d9117-dd9d-441d-8970-87e5e7e687a3",
|
||||
"type": "service_api",
|
||||
"is_anonymous": false,
|
||||
"session_id": "abc-123"
|
||||
},
|
||||
"created_at": 1726139644
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@ -192,12 +192,12 @@ const ModelLoadBalancingEntryModal: FC<ModelModalProps> = ({
|
||||
})
|
||||
const getSecretValues = useCallback((v: FormValue) => {
|
||||
return secretFormSchemas.reduce((prev, next) => {
|
||||
if (v[next.variable] === initialFormSchemasValue[next.variable])
|
||||
if (isEditMode && v[next.variable] && v[next.variable] === initialFormSchemasValue[next.variable])
|
||||
prev[next.variable] = '[__HIDDEN__]'
|
||||
|
||||
return prev
|
||||
}, {} as Record<string, string>)
|
||||
}, [initialFormSchemasValue, secretFormSchemas])
|
||||
}, [initialFormSchemasValue, isEditMode, secretFormSchemas])
|
||||
|
||||
// const handleValueChange = ({ __model_type, __model_name, ...v }: FormValue) => {
|
||||
const handleValueChange = (v: FormValue) => {
|
||||
@ -214,6 +214,7 @@ const ModelLoadBalancingEntryModal: FC<ModelModalProps> = ({
|
||||
...value,
|
||||
...getSecretValues(value),
|
||||
},
|
||||
entry?.id,
|
||||
)
|
||||
if (res.status === ValidatedStatus.Success) {
|
||||
// notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
|
||||
|
||||
@ -56,14 +56,14 @@ export const validateCredentials = async (predefined: boolean, provider: string,
|
||||
}
|
||||
}
|
||||
|
||||
export const validateLoadBalancingCredentials = async (predefined: boolean, provider: string, v: FormValue): Promise<{
|
||||
export const validateLoadBalancingCredentials = async (predefined: boolean, provider: string, v: FormValue, id?: string): Promise<{
|
||||
status: ValidatedStatus
|
||||
message?: string
|
||||
}> => {
|
||||
const { __model_name, __model_type, ...credentials } = v
|
||||
try {
|
||||
const res = await validateModelLoadBalancingCredentials({
|
||||
url: `/workspaces/current/model-providers/${provider}/models/load-balancing-configs/credentials-validate`,
|
||||
url: `/workspaces/current/model-providers/${provider}/models/load-balancing-configs/${id ? `${id}/` : ''}credentials-validate`,
|
||||
body: {
|
||||
model: __model_name,
|
||||
model_type: __model_type,
|
||||
|
||||
@ -269,8 +269,10 @@ const Result: FC<IResultProps> = ({
|
||||
}))
|
||||
},
|
||||
onWorkflowFinished: ({ data }) => {
|
||||
if (isTimeout)
|
||||
if (isTimeout) {
|
||||
notify({ type: 'warning', message: t('appDebug.warningMessage.timeoutExceeded') })
|
||||
return
|
||||
}
|
||||
if (data.error) {
|
||||
notify({ type: 'error', message: data.error })
|
||||
setWorkflowProcessData(produce(getWorkflowProcessData()!, (draft) => {
|
||||
@ -326,8 +328,10 @@ const Result: FC<IResultProps> = ({
|
||||
setCompletionRes(res.join(''))
|
||||
},
|
||||
onCompleted: () => {
|
||||
if (isTimeout)
|
||||
if (isTimeout) {
|
||||
notify({ type: 'warning', message: t('appDebug.warningMessage.timeoutExceeded') })
|
||||
return
|
||||
}
|
||||
setRespondingFalse()
|
||||
setMessageId(tempMessageId)
|
||||
onCompleted(getCompletionRes(), taskId, true)
|
||||
@ -338,8 +342,10 @@ const Result: FC<IResultProps> = ({
|
||||
setCompletionRes(res.join(''))
|
||||
},
|
||||
onError() {
|
||||
if (isTimeout)
|
||||
if (isTimeout) {
|
||||
notify({ type: 'warning', message: t('appDebug.warningMessage.timeoutExceeded') })
|
||||
return
|
||||
}
|
||||
setRespondingFalse()
|
||||
onCompleted(getCompletionRes(), taskId, false)
|
||||
isEnd = true
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { FC } from 'react'
|
||||
import type { FC, FormEvent } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
@ -41,11 +41,16 @@ const RunOnce: FC<IRunOnceProps> = ({
|
||||
onInputsChange(newInputs)
|
||||
}
|
||||
|
||||
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
onSend()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<section>
|
||||
{/* input form */}
|
||||
<form>
|
||||
<form onSubmit={onSubmit}>
|
||||
{promptConfig.prompt_variables.map(item => (
|
||||
<div className='w-full mt-4' key={item.key}>
|
||||
<label className='text-gray-900 text-sm font-medium'>{item.name}</label>
|
||||
@ -67,12 +72,6 @@ const RunOnce: FC<IRunOnceProps> = ({
|
||||
placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
|
||||
value={inputs[item.key]}
|
||||
onChange={(e) => { onInputsChange({ ...inputs, [item.key]: e.target.value }) }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
onSend()
|
||||
}
|
||||
}}
|
||||
maxLength={item.max_length || DEFAULT_VALUE_MAX_LEN}
|
||||
/>
|
||||
)}
|
||||
@ -127,8 +126,8 @@ const RunOnce: FC<IRunOnceProps> = ({
|
||||
<span className='text-[13px]'>{t('common.operation.clear')}</span>
|
||||
</Button>
|
||||
<Button
|
||||
type='submit'
|
||||
variant="primary"
|
||||
onClick={onSend}
|
||||
disabled={false}
|
||||
>
|
||||
<PlayIcon className="shrink-0 w-4 h-4 mr-1" aria-hidden="true" />
|
||||
|
||||
@ -90,7 +90,6 @@ const Blocks = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
needsDelay
|
||||
>
|
||||
<div className='group/item flex items-center w-full pl-3 pr-1 h-8 rounded-lg hover:bg-gray-50 cursor-pointer'>
|
||||
<BlockIcon
|
||||
|
||||
@ -70,15 +70,16 @@ export const useShortcuts = (): void => {
|
||||
})
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.c`, (e) => {
|
||||
const { showDebugAndPreviewPanel, showInputsPanel } = workflowStore.getState()
|
||||
if (shouldHandleShortcut(e) && !showDebugAndPreviewPanel && !showInputsPanel) {
|
||||
const { showDebugAndPreviewPanel } = workflowStore.getState()
|
||||
if (shouldHandleShortcut(e) && !showDebugAndPreviewPanel) {
|
||||
e.preventDefault()
|
||||
handleNodesCopy()
|
||||
}
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.v`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
const { showDebugAndPreviewPanel } = workflowStore.getState()
|
||||
if (shouldHandleShortcut(e) && !showDebugAndPreviewPanel) {
|
||||
e.preventDefault()
|
||||
handleNodesPaste()
|
||||
}
|
||||
@ -99,7 +100,8 @@ export const useShortcuts = (): void => {
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.z`, (e) => {
|
||||
if (shouldHandleShortcut(e)) {
|
||||
const { showDebugAndPreviewPanel } = workflowStore.getState()
|
||||
if (shouldHandleShortcut(e) && !showDebugAndPreviewPanel) {
|
||||
e.preventDefault()
|
||||
workflowHistoryShortcutsEnabled && handleHistoryBack()
|
||||
}
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { capitalize } from 'lodash-es'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiErrorWarningFill } from '@remixicon/react'
|
||||
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
Node,
|
||||
ValueSelector,
|
||||
VarType,
|
||||
} from '@/app/components/workflow/types'
|
||||
@ -11,65 +14,82 @@ import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { Line3 } from '@/app/components/base/icons/src/public/common'
|
||||
import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development'
|
||||
import { BubbleX, Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { getNodeInfoById, isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type VariableTagProps = {
|
||||
valueSelector: ValueSelector
|
||||
varType: VarType
|
||||
isShort?: boolean
|
||||
availableNodes?: Node[]
|
||||
}
|
||||
const VariableTag = ({
|
||||
valueSelector,
|
||||
varType,
|
||||
isShort,
|
||||
availableNodes,
|
||||
}: VariableTagProps) => {
|
||||
const nodes = useNodes<CommonNodeType>()
|
||||
const node = useMemo(() => {
|
||||
if (isSystemVar(valueSelector))
|
||||
return nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
if (isSystemVar(valueSelector)) {
|
||||
const startNode = availableNodes?.find(n => n.data.type === BlockEnum.Start)
|
||||
if (startNode)
|
||||
return startNode
|
||||
}
|
||||
return getNodeInfoById(availableNodes || nodes, valueSelector[0])
|
||||
}, [nodes, valueSelector, availableNodes])
|
||||
|
||||
return nodes.find(node => node.id === valueSelector[0])
|
||||
}, [nodes, valueSelector])
|
||||
const isEnv = isENV(valueSelector)
|
||||
const isChatVar = isConversationVar(valueSelector)
|
||||
const isValid = Boolean(node) || isEnv || isChatVar
|
||||
|
||||
const variableName = isSystemVar(valueSelector) ? valueSelector.slice(0).join('.') : valueSelector.slice(1).join('.')
|
||||
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='inline-flex items-center px-1.5 max-w-full h-6 text-xs rounded-md border-[0.5px] border-[rgba(16, 2440,0.08)] bg-white shadow-xs'>
|
||||
{!isEnv && !isChatVar && (
|
||||
<>
|
||||
<Tooltip popupContent={!isValid && t('workflow.errorMsg.invalidVariable')}>
|
||||
<div className={cn('inline-flex items-center px-1.5 max-w-full h-6 text-xs rounded-md border-[0.5px] border-[rgba(16, 2440,0.08)] bg-white shadow-xs',
|
||||
!isValid && 'border-red-400 !bg-[#FEF3F2]',
|
||||
)}>
|
||||
{(!isEnv && !isChatVar && <>
|
||||
{node && (
|
||||
<VarBlockIcon
|
||||
className='shrink-0 mr-0.5 text-text-secondary'
|
||||
type={node!.data.type}
|
||||
/>
|
||||
<>
|
||||
<VarBlockIcon
|
||||
type={BlockEnum.Start}
|
||||
/>
|
||||
<div
|
||||
className='max-w-[60px] truncate text-text-secondary font-medium'
|
||||
title={node?.data.title}
|
||||
>
|
||||
{node?.data.title}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
className='max-w-[60px] truncate text-text-secondary font-medium'
|
||||
title={node?.data.title}
|
||||
>
|
||||
{node?.data.title}
|
||||
</div>
|
||||
<Line3 className='shrink-0 mx-0.5' />
|
||||
<Variable02 className='shrink-0 mr-0.5 w-3.5 h-3.5 text-text-accent' />
|
||||
</>
|
||||
)}
|
||||
{isEnv && <Env className='shrink-0 mr-0.5 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||
<div
|
||||
className={cn('truncate text-text-accent font-medium', (isEnv || isChatVar) && 'text-text-secondary')}
|
||||
title={variableName}
|
||||
>
|
||||
{variableName}
|
||||
</>)}
|
||||
{isEnv && <Env className='shrink-0 mr-0.5 w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||
<div
|
||||
className={cn('truncate text-text-accent font-medium', (isEnv || isChatVar) && 'text-text-secondary')}
|
||||
title={variableName}
|
||||
>
|
||||
{variableName}
|
||||
</div>
|
||||
{
|
||||
varType && (
|
||||
<div className='shrink-0 ml-0.5 text-text-tertiary'>{capitalize(varType)}</div>
|
||||
)
|
||||
}
|
||||
{!isValid && <RiErrorWarningFill className='ml-0.5 w-3 h-3 text-[#D92D20]' />}
|
||||
</div>
|
||||
{
|
||||
!isShort && varType && (
|
||||
<div className='shrink-0 ml-0.5 text-text-tertiary'>{capitalize(varType)}</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -5,10 +5,12 @@ import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowDownSLine,
|
||||
RiCloseLine,
|
||||
RiErrorWarningFill,
|
||||
} from '@remixicon/react'
|
||||
import produce from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import RemoveButton from '../remove-button'
|
||||
import useAvailableVarList from '../../hooks/use-available-var-list'
|
||||
import VarReferencePopup from './var-reference-popup'
|
||||
import { getNodeInfoById, isConversationVar, isENV, isSystemVar } from './utils'
|
||||
import ConstantField from './constant-field'
|
||||
@ -27,13 +29,14 @@ import {
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useWorkflow,
|
||||
useWorkflowVariables,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
|
||||
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
|
||||
import AddButton from '@/app/components/base/button/add-button'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
|
||||
const TRIGGER_DEFAULT_WIDTH = 227
|
||||
|
||||
type Props = {
|
||||
@ -70,7 +73,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
onlyLeafNodeVar,
|
||||
filterVar = () => true,
|
||||
availableNodes: passedInAvailableNodes,
|
||||
availableVars,
|
||||
availableVars: passedInAvailableVars,
|
||||
isAddBtnTrigger,
|
||||
schema,
|
||||
valueTypePlaceHolder,
|
||||
@ -84,11 +87,12 @@ const VarReferencePicker: FC<Props> = ({
|
||||
} = store.getState()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const { getTreeLeafNodes, getBeforeNodesInSameBranch } = useWorkflow()
|
||||
const { getCurrentVariableType, getNodeAvailableVars } = useWorkflowVariables()
|
||||
const availableNodes = useMemo(() => {
|
||||
return passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId))
|
||||
}, [getBeforeNodesInSameBranch, getTreeLeafNodes, nodeId, onlyLeafNodeVar, passedInAvailableNodes])
|
||||
const { getCurrentVariableType } = useWorkflowVariables()
|
||||
const { availableNodes, availableVars } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar,
|
||||
passedInAvailableNodes,
|
||||
filterVar,
|
||||
})
|
||||
const startNode = availableNodes.find((node: any) => {
|
||||
return node.data.type === BlockEnum.Start
|
||||
})
|
||||
@ -107,19 +111,8 @@ const VarReferencePicker: FC<Props> = ({
|
||||
|
||||
const [varKindType, setVarKindType] = useState<VarKindType>(defaultVarKindType)
|
||||
const isConstant = isSupportConstantValue && varKindType === VarKindType.constant
|
||||
const outputVars = useMemo(() => {
|
||||
if (availableVars)
|
||||
return availableVars
|
||||
|
||||
const vars = getNodeAvailableVars({
|
||||
parentNode: iterationNode,
|
||||
beforeNodes: availableNodes,
|
||||
isChatMode,
|
||||
filterVar,
|
||||
})
|
||||
|
||||
return vars
|
||||
}, [iterationNode, availableNodes, isChatMode, filterVar, availableVars, getNodeAvailableVars])
|
||||
const outputVars = useMemo(() => (passedInAvailableVars || availableVars), [passedInAvailableVars, availableVars])
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
useEffect(() => {
|
||||
@ -194,7 +187,7 @@ const VarReferencePicker: FC<Props> = ({
|
||||
const handleVarReferenceChange = useCallback((value: ValueSelector, varInfo: Var) => {
|
||||
// sys var not passed to backend
|
||||
const newValue = produce(value, (draft) => {
|
||||
if (draft[1] && draft[1].startsWith('sys')) {
|
||||
if (draft[1] && draft[1].startsWith('sys.')) {
|
||||
draft.shift()
|
||||
const paths = draft[0].split('.')
|
||||
paths.forEach((p, i) => {
|
||||
@ -221,8 +214,16 @@ const VarReferencePicker: FC<Props> = ({
|
||||
isConstant: !!isConstant,
|
||||
})
|
||||
|
||||
const isEnv = isENV(value as ValueSelector)
|
||||
const isChatVar = isConversationVar(value as ValueSelector)
|
||||
const { isEnv, isChatVar, isValidVar } = useMemo(() => {
|
||||
const isEnv = isENV(value as ValueSelector)
|
||||
const isChatVar = isConversationVar(value as ValueSelector)
|
||||
const isValidVar = Boolean(outputVarNode) || isEnv || isChatVar
|
||||
return {
|
||||
isEnv,
|
||||
isChatVar,
|
||||
isValidVar,
|
||||
}
|
||||
}, [value, outputVarNode])
|
||||
|
||||
// 8(left/right-padding) + 14(icon) + 4 + 14 + 2 = 42 + 17 buff
|
||||
const availableWidth = triggerWidth - 56
|
||||
@ -301,39 +302,42 @@ const VarReferencePicker: FC<Props> = ({
|
||||
className='grow h-full'
|
||||
>
|
||||
<div ref={isSupportConstantValue ? triggerRef : null} className={cn('h-full', isSupportConstantValue && 'flex items-center pl-1 py-1 rounded-lg bg-gray-100')}>
|
||||
<div className={cn('h-full items-center px-1.5 rounded-[5px]', hasValue ? 'bg-white inline-flex' : 'flex')}>
|
||||
{hasValue
|
||||
? (
|
||||
<>
|
||||
{isShowNodeName && !isEnv && !isChatVar && (
|
||||
<div className='flex items-center'>
|
||||
<div className='p-[1px]'>
|
||||
<VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={outputVarNode?.type || BlockEnum.Start}
|
||||
/>
|
||||
<Tooltip popupContent={!isValidVar && hasValue && t('workflow.errorMsg.invalidVariable')}>
|
||||
<div className={cn('h-full items-center px-1.5 rounded-[5px]', hasValue ? 'bg-white inline-flex' : 'flex')}>
|
||||
{hasValue
|
||||
? (
|
||||
<>
|
||||
{isShowNodeName && !isEnv && !isChatVar && (
|
||||
<div className='flex items-center'>
|
||||
<div className='px-[1px] h-3'>
|
||||
{outputVarNode?.type && <VarBlockIcon
|
||||
className='!text-gray-900'
|
||||
type={outputVarNode.type}
|
||||
/>}
|
||||
</div>
|
||||
<div className='mx-0.5 text-xs font-medium text-gray-700 truncate' title={outputVarNode?.title} style={{
|
||||
maxWidth: maxNodeNameWidth,
|
||||
}}>{outputVarNode?.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
</div>
|
||||
<div className='mx-0.5 text-xs font-medium text-gray-700 truncate' title={outputVarNode?.title} style={{
|
||||
maxWidth: maxNodeNameWidth,
|
||||
}}>{outputVarNode?.title}</div>
|
||||
<Line3 className='mr-0.5'></Line3>
|
||||
)}
|
||||
<div className='flex items-center text-primary-600'>
|
||||
{!hasValue && <Variable02 className='w-3.5 h-3.5' />}
|
||||
{isEnv && <Env className='w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||
<div className={cn('ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && '!text-text-secondary')} title={varName} style={{
|
||||
maxWidth: maxVarNameWidth,
|
||||
}}>{varName}</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='flex items-center text-primary-600'>
|
||||
{!hasValue && <Variable02 className='w-3.5 h-3.5' />}
|
||||
{isEnv && <Env className='w-3.5 h-3.5 text-util-colors-violet-violet-600' />}
|
||||
{isChatVar && <BubbleX className='w-3.5 h-3.5 text-util-colors-teal-teal-700' />}
|
||||
<div className={cn('ml-0.5 text-xs font-medium truncate', (isEnv || isChatVar) && '!text-text-secondary')} title={varName} style={{
|
||||
maxWidth: maxVarNameWidth,
|
||||
}}>{varName}</div>
|
||||
</div>
|
||||
<div className='ml-0.5 text-xs font-normal text-gray-500 capitalize truncate' title={type} style={{
|
||||
maxWidth: maxTypeWidth,
|
||||
}}>{type}</div>
|
||||
</>
|
||||
)
|
||||
: <div className='text-[13px] font-normal text-gray-400'>{t('workflow.common.setVarValuePlaceholder')}</div>}
|
||||
</div>
|
||||
<div className='ml-0.5 text-xs font-normal text-gray-500 capitalize truncate' title={type} style={{
|
||||
maxWidth: maxTypeWidth,
|
||||
}}>{type}</div>
|
||||
{!isValidVar && <RiErrorWarningFill className='ml-0.5 w-3 h-3 text-[#D92D20]' />}
|
||||
</>
|
||||
)
|
||||
: <div className='text-[13px] font-normal text-gray-400'>{t('workflow.common.setVarValuePlaceholder')}</div>}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
</VarPickerWrap>
|
||||
|
||||
@ -4,12 +4,13 @@ import {
|
||||
useWorkflow,
|
||||
useWorkflowVariables,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import type { Node, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
type Params = {
|
||||
onlyLeafNodeVar?: boolean
|
||||
hideEnv?: boolean
|
||||
hideChatVar?: boolean
|
||||
filterVar: (payload: Var, selector: ValueSelector) => boolean
|
||||
passedInAvailableNodes?: Node[]
|
||||
}
|
||||
|
||||
const useAvailableVarList = (nodeId: string, {
|
||||
@ -17,6 +18,7 @@ const useAvailableVarList = (nodeId: string, {
|
||||
filterVar,
|
||||
hideEnv,
|
||||
hideChatVar,
|
||||
passedInAvailableNodes,
|
||||
}: Params = {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: () => true,
|
||||
@ -25,7 +27,7 @@ const useAvailableVarList = (nodeId: string, {
|
||||
const { getNodeAvailableVars } = useWorkflowVariables()
|
||||
const isChatMode = useIsChatMode()
|
||||
|
||||
const availableNodes = onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId)
|
||||
const availableNodes = passedInAvailableNodes || (onlyLeafNodeVar ? getTreeLeafNodes(nodeId) : getBeforeNodesInSameBranch(nodeId))
|
||||
|
||||
const {
|
||||
parentNode: iterationNode,
|
||||
|
||||
@ -222,6 +222,7 @@ const ConditionItem = ({
|
||||
<VariableTag
|
||||
valueSelector={condition.variable_selector || []}
|
||||
varType={condition.varType}
|
||||
availableNodes={availableNodes}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
RiArrowDownSLine,
|
||||
RiMenu4Line,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NodePanel from './node'
|
||||
import {
|
||||
BlockEnum,
|
||||
@ -37,7 +38,7 @@ type TracingNodeProps = {
|
||||
hideNodeProcessDetail?: boolean
|
||||
}
|
||||
|
||||
function buildLogTree(nodes: NodeTracing[]): TracingNodeProps[] {
|
||||
function buildLogTree(nodes: NodeTracing[], t: (key: string) => string): TracingNodeProps[] {
|
||||
const rootNodes: TracingNodeProps[] = []
|
||||
const parallelStacks: { [key: string]: TracingNodeProps } = {}
|
||||
const levelCounts: { [key: string]: number } = {}
|
||||
@ -58,7 +59,7 @@ function buildLogTree(nodes: NodeTracing[]): TracingNodeProps[] {
|
||||
const parentTitle = parentId ? parallelStacks[parentId]?.parallelTitle : ''
|
||||
const levelNumber = parentTitle ? parseInt(parentTitle.split('-')[1]) + 1 : 1
|
||||
const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''
|
||||
return `PARALLEL-${levelNumber}${letter}`
|
||||
return `${t('workflow.common.parallel')}-${levelNumber}${letter}`
|
||||
}
|
||||
|
||||
const getBranchTitle = (parentId: string | null, branchNum: number): string => {
|
||||
@ -67,7 +68,7 @@ function buildLogTree(nodes: NodeTracing[]): TracingNodeProps[] {
|
||||
const levelNumber = parentTitle ? parseInt(parentTitle.split('-')[1]) + 1 : 1
|
||||
const letter = parallelChildCounts[levelKey]?.size > 1 ? String.fromCharCode(64 + levelCounts[levelKey]) : ''
|
||||
const branchLetter = String.fromCharCode(64 + branchNum)
|
||||
return `BRANCH-${levelNumber}${letter}-${branchLetter}`
|
||||
return `${t('workflow.common.branch')}-${levelNumber}${letter}-${branchLetter}`
|
||||
}
|
||||
|
||||
// Count parallel children (for figuring out if we need to use letters)
|
||||
@ -163,7 +164,8 @@ const TracingPanel: FC<TracingPanelProps> = ({
|
||||
hideNodeInfo = false,
|
||||
hideNodeProcessDetail = false,
|
||||
}) => {
|
||||
const treeNodes = buildLogTree(list)
|
||||
const { t } = useTranslation()
|
||||
const treeNodes = buildLogTree(list, t)
|
||||
const [collapsedNodes, setCollapsedNodes] = useState<Set<string>>(new Set())
|
||||
const [hoveredParallel, setHoveredParallel] = useState<string | null>(null)
|
||||
|
||||
|
||||
@ -43,6 +43,7 @@ const LocaleLayout = ({
|
||||
data-public-sentry-dsn={process.env.NEXT_PUBLIC_SENTRY_DSN}
|
||||
data-public-maintenance-notice={process.env.NEXT_PUBLIC_MAINTENANCE_NOTICE}
|
||||
data-public-site-about={process.env.NEXT_PUBLIC_SITE_ABOUT}
|
||||
data-public-text-generation-timeout-ms={process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS}
|
||||
>
|
||||
<Topbar />
|
||||
<BrowserInitor>
|
||||
|
||||
@ -246,6 +246,13 @@ Thought: {{agent_scratchpad}}
|
||||
|
||||
export const VAR_REGEX = /\{\{(#[a-zA-Z0-9_-]{1,50}(\.[a-zA-Z_][a-zA-Z0-9_]{0,29}){1,10}#)\}\}/gi
|
||||
|
||||
export const TEXT_GENERATION_TIMEOUT_MS = 60000
|
||||
export let textGenerationTimeoutMs = 60000
|
||||
|
||||
if (process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS && process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS !== '')
|
||||
textGenerationTimeoutMs = parseInt(process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS)
|
||||
else if (globalThis.document?.body?.getAttribute('data-public-text-generation-timeout-ms') && globalThis.document.body.getAttribute('data-public-text-generation-timeout-ms') !== '')
|
||||
textGenerationTimeoutMs = parseInt(globalThis.document.body.getAttribute('data-public-text-generation-timeout-ms') as string)
|
||||
|
||||
export const TEXT_GENERATION_TIMEOUT_MS = textGenerationTimeoutMs
|
||||
|
||||
export const DISABLE_UPLOAD_IMAGE_AS_ICON = process.env.NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON === 'true'
|
||||
|
||||
@ -21,4 +21,6 @@ export NEXT_PUBLIC_SENTRY_DSN=${SENTRY_DSN}
|
||||
export NEXT_PUBLIC_SITE_ABOUT=${SITE_ABOUT}
|
||||
export NEXT_TELEMETRY_DISABLED=${NEXT_TELEMETRY_DISABLED}
|
||||
|
||||
export NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=${TEXT_GENERATION_TIMEOUT_MS}
|
||||
|
||||
pm2 start ./pm2.json --no-daemon
|
||||
|
||||
@ -93,6 +93,8 @@ const translation = {
|
||||
disconnect: 'Trennen',
|
||||
jumpToNode: 'Zu diesem Knoten springen',
|
||||
addParallelNode: 'Parallelen Knoten hinzufügen',
|
||||
parallel: 'PARALLEL',
|
||||
branch: 'ZWEIG',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: 'Umgebungsvariablen',
|
||||
|
||||
@ -281,6 +281,9 @@ const translation = {
|
||||
notSelectModel: 'Please choose a model',
|
||||
waitForImgUpload: 'Please wait for the image to upload',
|
||||
},
|
||||
warningMessage: {
|
||||
timeoutExceeded: 'Results are not displayed due to timeout. Please refer to the logs to gather complete results.',
|
||||
},
|
||||
chatSubTitle: 'Instructions',
|
||||
completionSubTitle: 'Prefix Prompt',
|
||||
promptTip:
|
||||
|
||||
@ -94,6 +94,8 @@ const translation = {
|
||||
disconnect: 'Disconnect',
|
||||
jumpToNode: 'Jump to this node',
|
||||
addParallelNode: 'Add Parallel Node',
|
||||
parallel: 'PARALLEL',
|
||||
branch: 'BRANCH',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: 'Environment Variables',
|
||||
|
||||
@ -93,6 +93,8 @@ const translation = {
|
||||
disconnect: 'Desconectar',
|
||||
jumpToNode: 'Saltar a este nodo',
|
||||
addParallelNode: 'Agregar nodo paralelo',
|
||||
parallel: 'PARALELO',
|
||||
branch: 'RAMA',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: 'Variables de Entorno',
|
||||
|
||||
@ -93,6 +93,8 @@ const translation = {
|
||||
jumpToNode: 'پرش به این گره',
|
||||
parallelRun: 'اجرای موازی',
|
||||
addParallelNode: 'افزودن گره موازی',
|
||||
parallel: 'موازی',
|
||||
branch: 'شاخه',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: 'متغیرهای محیطی',
|
||||
|
||||
@ -93,6 +93,8 @@ const translation = {
|
||||
disconnect: 'Déconnecter',
|
||||
jumpToNode: 'Aller à ce nœud',
|
||||
addParallelNode: 'Ajouter un nœud parallèle',
|
||||
parallel: 'PARALLÈLE',
|
||||
branch: 'BRANCHE',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: 'Variables d\'Environnement',
|
||||
|
||||
@ -96,6 +96,8 @@ const translation = {
|
||||
parallelRun: 'समानांतर रन',
|
||||
jumpToNode: 'इस नोड पर जाएं',
|
||||
addParallelNode: 'समानांतर नोड जोड़ें',
|
||||
parallel: 'समानांतर',
|
||||
branch: 'शाखा',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: 'पर्यावरण चर',
|
||||
|
||||
@ -97,6 +97,8 @@ const translation = {
|
||||
disconnect: 'Disconnettere',
|
||||
jumpToNode: 'Vai a questo nodo',
|
||||
addParallelNode: 'Aggiungi nodo parallelo',
|
||||
parallel: 'PARALLELO',
|
||||
branch: 'RAMO',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: 'Variabili d\'Ambiente',
|
||||
|
||||
@ -93,6 +93,8 @@ const translation = {
|
||||
disconnect: '切る',
|
||||
jumpToNode: 'このノードにジャンプします',
|
||||
addParallelNode: '並列ノードを追加',
|
||||
parallel: '並列',
|
||||
branch: 'ブランチ',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: '環境変数',
|
||||
|
||||
@ -93,6 +93,8 @@ const translation = {
|
||||
disconnect: '분리하다',
|
||||
jumpToNode: '이 노드로 이동',
|
||||
addParallelNode: '병렬 노드 추가',
|
||||
parallel: '병렬',
|
||||
branch: '브랜치',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: '환경 변수',
|
||||
|
||||
@ -93,6 +93,8 @@ const translation = {
|
||||
jumpToNode: 'Przejdź do tego węzła',
|
||||
disconnect: 'Odłączyć',
|
||||
addParallelNode: 'Dodaj węzeł równoległy',
|
||||
parallel: 'RÓWNOLEGŁY',
|
||||
branch: 'GAŁĄŹ',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: 'Zmienne Środowiskowe',
|
||||
|
||||
@ -93,6 +93,8 @@ const translation = {
|
||||
disconnect: 'Desligar',
|
||||
jumpToNode: 'Ir para este nó',
|
||||
addParallelNode: 'Adicionar nó paralelo',
|
||||
parallel: 'PARALELO',
|
||||
branch: 'RAMIFICAÇÃO',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: 'Variáveis de Ambiente',
|
||||
|
||||
@ -93,6 +93,8 @@ const translation = {
|
||||
disconnect: 'Deconecta',
|
||||
jumpToNode: 'Sari la acest nod',
|
||||
addParallelNode: 'Adăugare nod paralel',
|
||||
parallel: 'PARALEL',
|
||||
branch: 'RAMURĂ',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: 'Variabile de Mediu',
|
||||
|
||||
@ -93,6 +93,8 @@ const translation = {
|
||||
disconnect: 'Разъединять',
|
||||
jumpToNode: 'Перейти к этому узлу',
|
||||
addParallelNode: 'Добавить параллельный узел',
|
||||
parallel: 'ПАРАЛЛЕЛЬНЫЙ',
|
||||
branch: 'ВЕТКА',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: 'Переменные среды',
|
||||
|
||||
@ -93,6 +93,8 @@ const translation = {
|
||||
addParallelNode: 'Paralel Düğüm Ekle',
|
||||
disconnect: 'Ayırmak',
|
||||
parallelRun: 'Paralel Koşu',
|
||||
parallel: 'PARALEL',
|
||||
branch: 'DAL',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: 'Çevre Değişkenleri',
|
||||
|
||||
@ -93,6 +93,8 @@ const translation = {
|
||||
parallelRun: 'Паралельний біг',
|
||||
jumpToNode: 'Перейти до цього вузла',
|
||||
addParallelNode: 'Додати паралельний вузол',
|
||||
parallel: 'ПАРАЛЕЛЬНИЙ',
|
||||
branch: 'ГІЛКА',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: 'Змінні середовища',
|
||||
|
||||
@ -93,6 +93,8 @@ const translation = {
|
||||
disconnect: 'Ngắt kết nối',
|
||||
jumpToNode: 'Chuyển đến nút này',
|
||||
addParallelNode: 'Thêm nút song song',
|
||||
parallel: 'SONG SONG',
|
||||
branch: 'NHÁNH',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: 'Biến Môi Trường',
|
||||
|
||||
@ -94,6 +94,8 @@ const translation = {
|
||||
disconnect: '断开连接',
|
||||
jumpToNode: '跳转到节点',
|
||||
addParallelNode: '添加并行节点',
|
||||
parallel: '并行',
|
||||
branch: '分支',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: '环境变量',
|
||||
|
||||
@ -93,6 +93,8 @@ const translation = {
|
||||
disconnect: '斷開',
|
||||
jumpToNode: '跳轉到此節點',
|
||||
addParallelNode: '添加並行節點',
|
||||
parallel: '並行',
|
||||
branch: '分支',
|
||||
},
|
||||
env: {
|
||||
envPanelTitle: '環境變數',
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dify-web",
|
||||
"version": "0.8.0",
|
||||
"version": "0.8.2",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18.17.0"
|
||||
@ -44,6 +44,7 @@
|
||||
"classnames": "^2.3.2",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"crypto-js": "^4.2.0",
|
||||
"@svgdotjs/svg.js": "^3.2.4",
|
||||
"dayjs": "^1.11.7",
|
||||
"echarts": "^5.4.1",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
|
||||
@ -1489,6 +1489,11 @@
|
||||
dependencies:
|
||||
"@sinonjs/commons" "^3.0.0"
|
||||
|
||||
"@svgdotjs/svg.js@^3.2.4":
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz#4716be92a64c66b29921b63f7235fcfb953fb13a"
|
||||
integrity sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==
|
||||
|
||||
"@swc/counter@^0.1.3":
|
||||
version "0.1.3"
|
||||
resolved "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz"
|
||||
|
||||
Reference in New Issue
Block a user