fix: merge main

This commit is contained in:
CodingOnStar
2025-10-09 18:26:10 +08:00
191 changed files with 3623 additions and 1316 deletions

View File

@ -16,7 +16,7 @@ jest.mock('cmdk', () => ({
Item: ({ children, onSelect, value, className }: any) => (
<div
className={className}
onClick={() => onSelect && onSelect()}
onClick={() => onSelect?.()}
data-value={value}
data-testid={`command-item-${value}`}
>

View File

@ -4,6 +4,7 @@ import React, { useCallback, useRef, useState } from 'react'
import type { PopupProps } from './config-popup'
import ConfigPopup from './config-popup'
import cn from '@/utils/classnames'
import {
PortalToFollowElem,
PortalToFollowElemContent,
@ -45,7 +46,7 @@ const ConfigBtn: FC<Props> = ({
offset={12}
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
<div className="select-none">
<div className={cn('select-none', className)}>
{children}
</div>
</PortalToFollowElemTrigger>

View File

@ -28,7 +28,8 @@ const CSVUploader: FC<Props> = ({
const handleDragEnter = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
e.target !== dragRef.current && setDragging(true)
if (e.target !== dragRef.current)
setDragging(true)
}
const handleDragOver = (e: DragEvent) => {
e.preventDefault()
@ -37,7 +38,8 @@ const CSVUploader: FC<Props> = ({
const handleDragLeave = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
e.target === dragRef.current && setDragging(false)
if (e.target === dragRef.current)
setDragging(false)
}
const handleDrop = (e: DragEvent) => {
e.preventDefault()

View File

@ -348,7 +348,8 @@ const AppPublisher = ({
<SuggestedAction
className='flex-1'
onClick={() => {
publishedAt && handleOpenInExplore()
if (publishedAt)
handleOpenInExplore()
}}
disabled={!publishedAt || (systemFeatures.webapp_auth.enabled && !userCanAccessApp?.result)}
icon={<RiPlanetLine className='h-4 w-4' />}

View File

@ -40,7 +40,8 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
return
}
else {
titleError && setTitleError(false)
if (titleError)
setTitleError(false)
}
if (releaseNotes.length > RELEASE_NOTES_MAX_LENGTH) {
@ -52,7 +53,8 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
return
}
else {
releaseNotesError && setReleaseNotesError(false)
if (releaseNotesError)
setReleaseNotesError(false)
}
onPublish({ title, releaseNotes, id: versionInfo?.id })

View File

@ -0,0 +1,29 @@
import type { SVGProps } from 'react'
const CitationIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
{...props}
>
<path
d="M7 6h10M7 12h6M7 18h10"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M5 6c0-1.105.895-2 2-2h10c1.105 0 2 .895 2 2v12c0 1.105-.895 2-2 2H9l-4 3v-3H7"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
</svg>
)
export default CitationIcon

View File

@ -32,6 +32,19 @@ import { TransferMethod } from '@/types/app'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
const TEXT_MAX_LENGTH = 256
const CHECKBOX_DEFAULT_TRUE_VALUE = 'true'
const CHECKBOX_DEFAULT_FALSE_VALUE = 'false'
const getCheckboxDefaultSelectValue = (value: InputVar['default']) => {
if (typeof value === 'boolean')
return value ? CHECKBOX_DEFAULT_TRUE_VALUE : CHECKBOX_DEFAULT_FALSE_VALUE
if (typeof value === 'string')
return value.toLowerCase() === CHECKBOX_DEFAULT_TRUE_VALUE ? CHECKBOX_DEFAULT_TRUE_VALUE : CHECKBOX_DEFAULT_FALSE_VALUE
return CHECKBOX_DEFAULT_FALSE_VALUE
}
const parseCheckboxSelectValue = (value: string) =>
value === CHECKBOX_DEFAULT_TRUE_VALUE
export type IConfigModalProps = {
isCreate?: boolean
@ -66,7 +79,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
try {
return JSON.stringify(JSON.parse(tempPayload.json_schema).properties, null, 2)
}
catch (_e) {
catch {
return ''
}
}, [tempPayload.json_schema])
@ -110,7 +123,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
}
handlePayloadChange('json_schema')(JSON.stringify(res, null, 2))
}
catch (_e) {
catch {
return null
}
}, [handlePayloadChange])
@ -198,6 +211,8 @@ const ConfigModal: FC<IConfigModalProps> = ({
handlePayloadChange('variable')(e.target.value)
}, [handlePayloadChange, t])
const checkboxDefaultSelectValue = useMemo(() => getCheckboxDefaultSelectValue(tempPayload.default), [tempPayload.default])
const handleConfirm = () => {
const moreInfo = tempPayload.variable === payload?.variable
? undefined
@ -324,6 +339,23 @@ const ConfigModal: FC<IConfigModalProps> = ({
</Field>
)}
{type === InputVarType.checkbox && (
<Field title={t('appDebug.variableConfig.defaultValue')}>
<SimpleSelect
className="w-full"
optionWrapClassName="max-h-[140px] overflow-y-auto"
items={[
{ value: CHECKBOX_DEFAULT_TRUE_VALUE, name: t('appDebug.variableConfig.startChecked') },
{ value: CHECKBOX_DEFAULT_FALSE_VALUE, name: t('appDebug.variableConfig.noDefaultSelected') },
]}
defaultValue={checkboxDefaultSelectValue}
onSelect={item => handlePayloadChange('default')(parseCheckboxSelectValue(String(item.value)))}
placeholder={t('appDebug.variableConfig.selectDefaultValue')}
allowSearch={false}
/>
</Field>
)}
{type === InputVarType.select && (
<>
<Field title={t('appDebug.variableConfig.options')}>

View File

@ -480,7 +480,7 @@ const Configuration: FC = () => {
Toast.notify({ type: 'warning', message: `${t('common.modelProvider.parametersInvalidRemoved')}: ${Object.entries(removedDetails).map(([k, reason]) => `${k} (${reason})`).join(', ')}` })
setCompletionParams(filtered)
}
catch (e) {
catch {
Toast.notify({ type: 'error', message: t('common.error') })
setCompletionParams({})
}

View File

@ -192,7 +192,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
<Button
variant="primary"
disabled={canNotRun}
onClick={() => onSend && onSend()}
onClick={() => onSend?.()}
className="w-[96px]">
<RiPlayLargeFill className="mr-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
{t('appDebug.inputs.run')}
@ -203,7 +203,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
<Button
variant="primary"
disabled={canNotRun}
onClick={() => onSend && onSend()}
onClick={() => onSend?.()}
className="w-[96px]">
<RiPlayLargeFill className="mr-0.5 h-4 w-4 shrink-0" aria-hidden="true" />
{t('appDebug.inputs.run')}

View File

@ -38,7 +38,8 @@ const Uploader: FC<Props> = ({
const handleDragEnter = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
e.target !== dragRef.current && setDragging(true)
if (e.target !== dragRef.current)
setDragging(true)
}
const handleDragOver = (e: DragEvent) => {
e.preventDefault()
@ -47,7 +48,8 @@ const Uploader: FC<Props> = ({
const handleDragLeave = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
e.target === dragRef.current && setDragging(false)
if (e.target === dragRef.current)
setDragging(false)
}
const handleDrop = (e: DragEvent) => {
e.preventDefault()

View File

@ -107,7 +107,8 @@ const Chart: React.FC<IChartProps> = ({
const { t } = useTranslation()
const statistics = chartData.data
const statisticsLen = statistics.length
const extraDataForMarkLine = new Array(statisticsLen >= 2 ? statisticsLen - 2 : statisticsLen).fill('1')
const markLineLength = statisticsLen >= 2 ? statisticsLen - 2 : statisticsLen
const extraDataForMarkLine = Array.from({ length: markLineLength }, () => '1')
extraDataForMarkLine.push('')
extraDataForMarkLine.unshift('')

View File

@ -127,7 +127,7 @@ export default class AudioPlayer {
}
catch {
this.isLoadData = false
this.callback && this.callback('error')
this.callback?.('error')
}
}
@ -137,15 +137,14 @@ export default class AudioPlayer {
if (this.audioContext.state === 'suspended') {
this.audioContext.resume().then((_) => {
this.audio.play()
this.callback && this.callback('play')
this.callback?.('play')
})
}
else if (this.audio.ended) {
this.audio.play()
this.callback && this.callback('play')
this.callback?.('play')
}
if (this.callback)
this.callback('play')
this.callback?.('play')
}
else {
this.isLoadData = true
@ -189,24 +188,24 @@ export default class AudioPlayer {
if (this.audio.paused) {
this.audioContext.resume().then((_) => {
this.audio.play()
this.callback && this.callback('play')
this.callback?.('play')
})
}
else if (this.audio.ended) {
this.audio.play()
this.callback && this.callback('play')
this.callback?.('play')
}
else if (this.audio.played) { /* empty */ }
else {
this.audio.play()
this.callback && this.callback('play')
this.callback?.('play')
}
}
}
public pauseAudio() {
this.callback && this.callback('paused')
this.callback?.('paused')
this.audio.pause()
this.audioContext.suspend()
}

View File

@ -128,7 +128,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
const localState = localStorage.getItem('webappSidebarCollapse')
return localState === 'collapsed'
}
catch (e) {
catch {
// localStorage may be disabled in private browsing mode or by security settings
// fallback to default value
return false
@ -142,7 +142,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
try {
localStorage.setItem('webappSidebarCollapse', state ? 'collapsed' : 'expanded')
}
catch (e) {
catch {
// localStorage may be disabled, continue without persisting state
}
}
@ -235,13 +235,15 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
}
}
if(item.checkbox) {
if (item.checkbox) {
const preset = initInputs[item.checkbox.variable] === true
return {
...item.checkbox,
default: false,
default: preset || item.default || item.checkbox.default,
type: 'checkbox',
}
}
if (item.select) {
const isInputInOptions = item.select.options.includes(initInputs[item.select.variable])
return {

View File

@ -101,10 +101,14 @@ const Answer: FC<AnswerProps> = ({
}, [])
const handleSwitchSibling = useCallback((direction: 'prev' | 'next') => {
if (direction === 'prev')
item.prevSibling && switchSibling?.(item.prevSibling)
else
item.nextSibling && switchSibling?.(item.nextSibling)
if (direction === 'prev') {
if (item.prevSibling)
switchSibling?.(item.prevSibling)
}
else {
if (item.nextSibling)
switchSibling?.(item.nextSibling)
}
}, [switchSibling, item.prevSibling, item.nextSibling])
return (

View File

@ -73,10 +73,14 @@ const Question: FC<QuestionProps> = ({
}, [content])
const handleSwitchSibling = useCallback((direction: 'prev' | 'next') => {
if (direction === 'prev')
item.prevSibling && switchSibling?.(item.prevSibling)
else
item.nextSibling && switchSibling?.(item.nextSibling)
if (direction === 'prev') {
if (item.prevSibling)
switchSibling?.(item.prevSibling)
}
else {
if (item.nextSibling)
switchSibling?.(item.nextSibling)
}
}, [switchSibling, item.prevSibling, item.nextSibling])
const getContentWidth = () => {

View File

@ -195,13 +195,16 @@ export const useEmbeddedChatbot = () => {
type: 'number',
}
}
if (item.checkbox) {
const preset = initInputs[item.checkbox.variable] === true
return {
...item.checkbox,
default: false,
default: preset || item.default || item.checkbox.default,
type: 'checkbox',
}
}
if (item.select) {
const isInputInOptions = item.select.options.includes(initInputs[item.select.variable])
return {

View File

@ -124,7 +124,7 @@ export const parseDateWithFormat = (dateString: string, format?: string): Dayjs
}
// Format date output with localization support
export const formatDateForOutput = (date: Dayjs, includeTime: boolean = false, locale: string = 'en-US'): string => {
export const formatDateForOutput = (date: Dayjs, includeTime: boolean = false, _locale: string = 'en-US'): string => {
if (!date || !date.isValid()) return ''
if (includeTime) {

View File

@ -47,7 +47,10 @@ export default function Drawer({
<Dialog
unmount={unmount}
open={isOpen}
onClose={() => !clickOutsideNotOpen && onClose()}
onClose={() => {
if (!clickOutsideNotOpen)
onClose()
}}
className={cn('fixed inset-0 z-[30] overflow-y-auto', dialogClassName)}
>
<div className={cn('flex h-screen w-screen justify-end', positionCenter && '!justify-center')}>
@ -55,7 +58,8 @@ export default function Drawer({
<DialogBackdrop
className={cn('fixed inset-0 z-[40]', mask && 'bg-black/30', dialogBackdropClassName)}
onClick={() => {
!clickOutsideNotOpen && onClose()
if (!clickOutsideNotOpen)
onClose()
}}
/>
<div className={cn('relative z-[50] flex w-full max-w-sm flex-col justify-between overflow-hidden bg-components-panel-bg p-6 text-left align-middle shadow-xl', panelClassName)}>
@ -80,11 +84,11 @@ export default function Drawer({
<Button
className='mr-2'
onClick={() => {
onCancel && onCancel()
onCancel?.()
}}>{t('common.operation.cancel')}</Button>
<Button
onClick={() => {
onOk && onOk()
onOk?.()
}}>{t('common.operation.save')}</Button>
</div>)}
</div>

View File

@ -45,7 +45,7 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
<Divider className='mb-0 mt-3' />
<div className='flex w-full items-center justify-center gap-2 p-3'>
<Button className='w-full' onClick={() => {
onClose && onClose()
onClose?.()
}}>
{t('app.iconPicker.cancel')}
</Button>
@ -54,7 +54,7 @@ const EmojiPicker: FC<IEmojiPickerProps> = ({
variant="primary"
className='w-full'
onClick={() => {
onSelect && onSelect(selectedEmoji, selectedBackground!)
onSelect?.(selectedEmoji, selectedBackground!)
}}>
{t('app.iconPicker.ok')}
</Button>

View File

@ -33,7 +33,10 @@ const SelectField = ({
<PureSelect
value={field.state.value}
options={options}
onChange={value => field.handleChange(value)}
onChange={(value) => {
field.handleChange(value)
onChange?.(value)
}}
{...selectProps}
/>
</div>

View File

@ -62,7 +62,7 @@ const ImageList: FC<ImageListProps> = ({
{item.progress === -1 && (
<RefreshCcw01
className="h-5 w-5 text-white"
onClick={() => onReUpload && onReUpload(item._id)}
onClick={() => onReUpload?.(item._id)}
/>
)}
</div>
@ -122,7 +122,7 @@ const ImageList: FC<ImageListProps> = ({
'rounded-2xl shadow-lg hover:bg-state-base-hover',
item.progress === -1 ? 'flex' : 'hidden group-hover:flex',
)}
onClick={() => onRemove && onRemove(item._id)}
onClick={() => onRemove?.(item._id)}
>
<RiCloseLine className="h-3 w-3 text-text-tertiary" />
</button>

View File

@ -20,7 +20,7 @@ const isBase64 = (str: string): boolean => {
try {
return btoa(atob(str)) === str
}
catch (err) {
catch {
return false
}
}

View File

@ -8,12 +8,14 @@ import {
import ActionButton from '@/app/components/base/action-button'
import CopyIcon from '@/app/components/base/copy-icon'
import SVGBtn from '@/app/components/base/svg'
import Flowchart from '@/app/components/base/mermaid'
import { Theme } from '@/types/app'
import useTheme from '@/hooks/use-theme'
import SVGRenderer from '../svg-gallery' // Assumes svg-gallery.tsx is in /base directory
import MarkdownMusic from '@/app/components/base/markdown-blocks/music'
import ErrorBoundary from '@/app/components/base/markdown/error-boundary'
import dynamic from 'next/dynamic'
const Flowchart = dynamic(() => import('@/app/components/base/mermaid'), { ssr: false })
// Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
const capitalizationLanguageNameMap: Record<string, string> = {
@ -125,7 +127,7 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
// Store event handlers in useMemo to avoid recreating them
const echartsEvents = useMemo(() => ({
finished: (params: EChartsEventParams) => {
finished: (_params: EChartsEventParams) => {
// Limit finished event frequency to avoid infinite loops
finishedEventCountRef.current++
if (finishedEventCountRef.current > 3) {

View File

@ -1,25 +1,11 @@
import ReactMarkdown from 'react-markdown'
import dynamic from 'next/dynamic'
import 'katex/dist/katex.min.css'
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 { flow } from 'lodash-es'
import cn from '@/utils/classnames'
import { customUrlTransform, preprocessLaTeX, preprocessThinkTag } from './markdown-utils'
import {
AudioBlock,
CodeBlock,
Img,
Link,
MarkdownButton,
MarkdownForm,
Paragraph,
ScriptBlock,
ThinkBlock,
VideoBlock,
} from '@/app/components/base/markdown-blocks'
import { preprocessLaTeX, preprocessThinkTag } from './markdown-utils'
import type { ReactMarkdownWrapperProps } from './react-markdown-wrapper'
const ReactMarkdown = dynamic(() => import('./react-markdown-wrapper').then(mod => mod.ReactMarkdownWrapper), { ssr: false })
/**
* @fileoverview Main Markdown rendering component.
@ -31,9 +17,7 @@ import {
export type MarkdownProps = {
content: string
className?: string
customDisallowedElements?: string[]
customComponents?: Record<string, React.ComponentType<any>>
}
} & Pick<ReactMarkdownWrapperProps, 'customComponents' | 'customDisallowedElements'>
export const Markdown = (props: MarkdownProps) => {
const { customComponents = {} } = props
@ -44,53 +28,7 @@ export const Markdown = (props: MarkdownProps) => {
return (
<div className={cn('markdown-body', '!text-text-primary', props.className)}>
<ReactMarkdown
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: any) => {
const iterate = (node: any) => {
if (node.type === 'element' && node.properties?.ref)
delete node.properties.ref
if (node.type === 'element' && !/^[a-z][a-z0-9]*$/i.test(node.tagName)) {
node.type = 'text'
node.value = `<${node.tagName}`
}
if (node.children)
node.children.forEach(iterate)
}
tree.children.forEach(iterate)
}
},
]}
urlTransform={customUrlTransform}
disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]}
components={{
code: CodeBlock,
img: Img,
video: VideoBlock,
audio: AudioBlock,
a: Link,
p: Paragraph,
button: MarkdownButton,
form: MarkdownForm,
script: ScriptBlock as any,
details: ThinkBlock,
...customComponents,
}}
>
{/* Markdown detect has problem. */}
{latexContent}
</ReactMarkdown>
<ReactMarkdown latexContent={latexContent} customComponents={customComponents} customDisallowedElements={props.customDisallowedElements} />
</div>
)
}

View File

@ -0,0 +1,82 @@
import ReactMarkdown from 'react-markdown'
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 AudioBlock from '@/app/components/base/markdown-blocks/audio-block'
import Img from '@/app/components/base/markdown-blocks/img'
import Link from '@/app/components/base/markdown-blocks/link'
import MarkdownButton from '@/app/components/base/markdown-blocks/button'
import MarkdownForm from '@/app/components/base/markdown-blocks/form'
import Paragraph from '@/app/components/base/markdown-blocks/paragraph'
import ScriptBlock from '@/app/components/base/markdown-blocks/script-block'
import ThinkBlock from '@/app/components/base/markdown-blocks/think-block'
import VideoBlock from '@/app/components/base/markdown-blocks/video-block'
import { customUrlTransform } from './markdown-utils'
import type { FC } from 'react'
import dynamic from 'next/dynamic'
const CodeBlock = dynamic(() => import('@/app/components/base/markdown-blocks/code-block'), { ssr: false })
export type ReactMarkdownWrapperProps = {
latexContent: any
customDisallowedElements?: string[]
customComponents?: Record<string, React.ComponentType<any>>
}
export const ReactMarkdownWrapper: FC<ReactMarkdownWrapperProps> = (props) => {
const { customComponents, latexContent } = props
return (
<ReactMarkdown
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: any) => {
const iterate = (node: any) => {
if (node.type === 'element' && node.properties?.ref)
delete node.properties.ref
if (node.type === 'element' && !/^[a-z][a-z0-9]*$/i.test(node.tagName)) {
node.type = 'text'
node.value = `<${node.tagName}`
}
if (node.children)
node.children.forEach(iterate)
}
tree.children.forEach(iterate)
}
},
]}
urlTransform={customUrlTransform}
disallowedElements={['iframe', 'head', 'html', 'meta', 'link', 'style', 'body', ...(props.customDisallowedElements || [])]}
components={{
code: CodeBlock,
img: Img,
video: VideoBlock,
audio: AudioBlock,
a: Link,
p: Paragraph,
button: MarkdownButton,
form: MarkdownForm,
script: ScriptBlock as any,
details: ThinkBlock,
...customComponents,
}}
>
{/* Markdown detect has problem. */}
{latexContent}
</ReactMarkdown>
)
}

View File

@ -60,7 +60,7 @@ export function svgToBase64(svgGraph: string): Promise<string> {
reader.readAsDataURL(blob)
})
}
catch (error) {
catch {
return Promise.resolve('')
}
}

View File

@ -10,9 +10,7 @@ const usePagination = ({
edgePageCount,
middlePagesSiblingCount,
}: IPaginationProps): IUsePagination => {
const pages = new Array(totalPages)
.fill(0)
.map((_, i) => i + 1)
const pages = React.useMemo(() => Array.from({ length: totalPages }, (_, i) => i + 1), [totalPages])
const hasPreviousPage = currentPage > 1
const hasNextPage = currentPage < totalPages

View File

@ -57,7 +57,34 @@ const CustomizedPagination: FC<Props> = ({
if (isNaN(Number.parseInt(value)))
return setInputValue('')
setInputValue(Number.parseInt(value))
handlePaging(value)
}
const handleInputConfirm = () => {
if (inputValue !== '' && String(inputValue) !== String(current + 1)) {
handlePaging(String(inputValue))
return
}
if (inputValue === '')
setInputValue(current + 1)
setShowInput(false)
}
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault()
handleInputConfirm()
}
else if (e.key === 'Escape') {
e.preventDefault()
setInputValue(current + 1)
setShowInput(false)
}
}
const handleInputBlur = () => {
handleInputConfirm()
}
return (
@ -105,7 +132,8 @@ const CustomizedPagination: FC<Props> = ({
autoFocus
value={inputValue}
onChange={handleInputChange}
onBlur={() => setShowInput(false)}
onKeyDown={handleInputKeyDown}
onBlur={handleInputBlur}
/>
)}
<Pagination.NextButton

View File

@ -37,13 +37,16 @@ export default function CustomPopover({
const timeOutRef = useRef<number | null>(null)
const onMouseEnter = (isOpen: boolean) => {
timeOutRef.current && window.clearTimeout(timeOutRef.current)
!isOpen && buttonRef.current?.click()
if (timeOutRef.current != null)
window.clearTimeout(timeOutRef.current)
if (!isOpen)
buttonRef.current?.click()
}
const onMouseLeave = (isOpen: boolean) => {
timeOutRef.current = window.setTimeout(() => {
isOpen && buttonRef.current?.click()
if (isOpen)
buttonRef.current?.click()
}, timeoutDuration)
}

View File

@ -43,7 +43,7 @@ export default function LocaleSigninSelect({
className={'group flex w-full items-center rounded-lg px-3 py-2 text-sm text-text-secondary data-[active]:bg-state-base-hover'}
onClick={(evt) => {
evt.preventDefault()
onChange && onChange(item.value)
onChange?.(item.value)
}}
>
{item.name}

View File

@ -43,7 +43,7 @@ export default function Select({
className={'group flex w-full items-center rounded-lg px-3 py-2 text-sm text-text-secondary data-[active]:bg-state-base-hover'}
onClick={(evt) => {
evt.preventDefault()
onChange && onChange(item.value)
onChange?.(item.value)
}}
>
{item.name}

View File

@ -97,10 +97,13 @@ const Panel = (props: PanelProps) => {
const removeTagIDs = value.filter(v => !selectedTagIDs.includes(v))
const selectedTags = tagList.filter(tag => selectedTagIDs.includes(tag.id))
onCacheUpdate(selectedTags)
Promise.all([
...(addTagIDs.length ? [bind(addTagIDs)] : []),
...[removeTagIDs.length ? removeTagIDs.map(tagID => unbind(tagID)) : []],
]).finally(() => {
const operations: Promise<unknown>[] = []
if (addTagIDs.length)
operations.push(bind(addTagIDs))
if (removeTagIDs.length)
operations.push(...removeTagIDs.map(tagID => unbind(tagID)))
Promise.all(operations).finally(() => {
if (onChange)
onChange()
})

View File

@ -81,7 +81,8 @@ const VoiceInput = ({
setStartRecord(false)
setStartConvert(true)
recorder.current.stop()
drawRecordId.current && cancelAnimationFrame(drawRecordId.current)
if (drawRecordId.current)
cancelAnimationFrame(drawRecordId.current)
drawRecordId.current = null
const canvas = canvasRef.current!
const ctx = ctxRef.current!

View File

@ -34,7 +34,8 @@ const Uploader: FC<Props> = ({
const handleDragEnter = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
e.target !== dragRef.current && setDragging(true)
if (e.target !== dragRef.current)
setDragging(true)
}
const handleDragOver = (e: DragEvent) => {
e.preventDefault()
@ -43,7 +44,8 @@ const Uploader: FC<Props> = ({
const handleDragLeave = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
e.target === dragRef.current && setDragging(false)
if (e.target === dragRef.current)
setDragging(false)
}
const handleDrop = (e: DragEvent) => {
e.preventDefault()

View File

@ -185,7 +185,8 @@ const FileUploader = ({
const handleDragEnter = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
e.target !== dragRef.current && setDragging(true)
if (e.target !== dragRef.current)
setDragging(true)
}
const handleDragOver = (e: DragEvent) => {
e.preventDefault()
@ -194,7 +195,8 @@ const FileUploader = ({
const handleDragLeave = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
e.target === dragRef.current && setDragging(false)
if (e.target === dragRef.current)
setDragging(false)
}
type FileWithPath = {
relativePath?: string

View File

@ -568,9 +568,9 @@ const StepTwo = ({
params,
{
onSuccess(data) {
updateIndexingTypeCache && updateIndexingTypeCache(indexType as string)
updateResultCache && updateResultCache(data)
updateRetrievalMethodCache && updateRetrievalMethodCache(retrievalConfig.search_method as string)
updateIndexingTypeCache?.(indexType as string)
updateResultCache?.(data)
updateRetrievalMethodCache?.(retrievalConfig.search_method as string)
},
},
)
@ -578,17 +578,18 @@ const StepTwo = ({
else {
await createDocumentMutation.mutateAsync(params, {
onSuccess(data) {
updateIndexingTypeCache && updateIndexingTypeCache(indexType as string)
updateResultCache && updateResultCache(data)
updateRetrievalMethodCache && updateRetrievalMethodCache(retrievalConfig.search_method as string)
updateIndexingTypeCache?.(indexType as string)
updateResultCache?.(data)
updateRetrievalMethodCache?.(retrievalConfig.search_method as string)
},
})
}
if (mutateDatasetRes)
mutateDatasetRes()
invalidDatasetList()
onStepChange && onStepChange(+1)
isSetting && onSave && onSave()
onStepChange?.(+1)
if (isSetting)
onSave?.()
}
useEffect(() => {
@ -1026,7 +1027,7 @@ const StepTwo = ({
{!isSetting
? (
<div className='mt-8 flex items-center py-2'>
<Button onClick={() => onStepChange && onStepChange(-1)}>
<Button onClick={() => onStepChange?.(-1)}>
<RiArrowLeftLine className='mr-1 h-4 w-4' />
{t('datasetCreation.stepTwo.previousStep')}
</Button>

View File

@ -7,7 +7,6 @@ import DocumentFileIcon from '@/app/components/datasets/common/document-file-ico
import cn from '@/utils/classnames'
import type { CustomFile as File, FileItem } from '@/models/datasets'
import { ToastContext } from '@/app/components/base/toast'
import SimplePieChart from '@/app/components/base/simple-pie-chart'
import { upload } from '@/service/base'
import I18n from '@/context/i18n'
import { LanguagesSupported } from '@/i18n-config/language'
@ -17,6 +16,9 @@ import useTheme from '@/hooks/use-theme'
import { useFileUploadConfig } from '@/service/use-common'
import { useDataSourceStore, useDataSourceStoreWithSelector } from '../store'
import produce from 'immer'
import dynamic from 'next/dynamic'
const SimplePieChart = dynamic(() => import('@/app/components/base/simple-pie-chart'), { ssr: false })
const FILES_NUMBER_LIMIT = 20
@ -198,7 +200,8 @@ const LocalFile = ({
const handleDragEnter = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
e.target !== dragRef.current && setDragging(true)
if (e.target !== dragRef.current)
setDragging(true)
}
const handleDragOver = (e: DragEvent) => {
e.preventDefault()
@ -207,7 +210,8 @@ const LocalFile = ({
const handleDragLeave = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
e.target === dragRef.current && setDragging(false)
if (e.target === dragRef.current)
setDragging(false)
}
const handleDrop = useCallback((e: DragEvent) => {

View File

@ -45,10 +45,13 @@ const CrawledResult = ({
const handleItemCheckChange = useCallback((item: CrawlResultItem) => {
return (checked: boolean) => {
if (checked)
isMultipleChoice ? onSelectedChange([...checkedList, item]) : onSelectedChange([item])
else
onSelectedChange(checkedList.filter(checkedItem => checkedItem.source_url !== item.source_url))
if (checked) {
if (isMultipleChoice)
onSelectedChange([...checkedList, item])
else
onSelectedChange([item])
}
else { onSelectedChange(checkedList.filter(checkedItem => checkedItem.source_url !== item.source_url)) }
}
}, [checkedList, onSelectedChange, isMultipleChoice])

View File

@ -326,7 +326,10 @@ const CreateFormPipeline = () => {
}, [])
const handleSubmit = useCallback((data: Record<string, any>) => {
isPreview.current ? handlePreviewChunks(data) : handleProcess(data)
if (isPreview.current)
handlePreviewChunks(data)
else
handleProcess(data)
}, [handlePreviewChunks, handleProcess])
const handlePreviewFileChange = useCallback((file: DocumentItem) => {

View File

@ -99,7 +99,8 @@ const CSVUploader: FC<Props> = ({
const handleDragEnter = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
e.target !== dragRef.current && setDragging(true)
if (e.target !== dragRef.current)
setDragging(true)
}
const handleDragOver = (e: DragEvent) => {
e.preventDefault()
@ -108,7 +109,8 @@ const CSVUploader: FC<Props> = ({
const handleDragLeave = (e: DragEvent) => {
e.preventDefault()
e.stopPropagation()
e.target === dragRef.current && setDragging(false)
if (e.target === dragRef.current)
setDragging(false)
}
const handleDrop = (e: DragEvent) => {
e.preventDefault()

View File

@ -284,7 +284,8 @@ const Completed: FC<ICompletedProps> = ({
onSuccess: () => {
notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') })
resetList()
!segId && setSelectedSegmentIds([])
if (!segId)
setSelectedSegmentIds([])
},
onError: () => {
notify({ type: 'error', message: t('common.actionMsg.modifiedUnsuccessfully') })
@ -438,7 +439,8 @@ const Completed: FC<ICompletedProps> = ({
}
else {
resetList()
currentPage !== totalPages && setCurrentPage(totalPages)
if (currentPage !== totalPages)
setCurrentPage(totalPages)
}
}, [segmentListData, limit, currentPage, resetList])
@ -491,7 +493,8 @@ const Completed: FC<ICompletedProps> = ({
}
else {
resetChildList()
currentPage !== totalPages && setCurrentPage(totalPages)
if (currentPage !== totalPages)
setCurrentPage(totalPages)
}
}, [childChunkListData, limit, currentPage, resetChildList])

View File

@ -66,7 +66,7 @@ export const FieldInfo: FC<IFieldInfoProps> = ({
? displayedValue
: inputType === 'select'
? <SimpleSelect
onSelect={({ value }) => onUpdate && onUpdate(value as string)}
onSelect={({ value }) => onUpdate?.(value as string)}
items={selectOptions}
defaultValue={value}
className={s.select}
@ -75,7 +75,7 @@ export const FieldInfo: FC<IFieldInfoProps> = ({
/>
: inputType === 'textarea'
? <AutoHeightTextarea
onChange={e => onUpdate && onUpdate(e.target.value)}
onChange={e => onUpdate?.(e.target.value)}
value={value}
className={s.textArea}
placeholder={`${t('datasetDocuments.metadata.placeholder.add')}${label}`}

View File

@ -148,7 +148,10 @@ const PipelineSettings = ({
}, [])
const handleSubmit = useCallback((data: Record<string, any>) => {
isPreview.current ? handlePreviewChunks(data) : handleProcess(data)
if (isPreview.current)
handlePreviewChunks(data)
else
handleProcess(data)
}, [handlePreviewChunks, handleProcess])
if (isFetchingLastRunData) {

View File

@ -80,7 +80,8 @@ const TextAreaWithButton = ({
onUpdateList?.()
}
setLoading(false)
_onSubmit && _onSubmit()
if (_onSubmit)
_onSubmit()
}
const externalRetrievalTestingOnSubmit = async () => {

View File

@ -157,12 +157,12 @@ const DatasetCard = ({
data-disable-nprogress={true}
onClick={(e) => {
e.preventDefault()
isExternalProvider
? push(`/datasets/${dataset.id}/hitTesting`)
// eslint-disable-next-line sonarjs/no-nested-conditional
: isPipelineUnpublished
? push(`/datasets/${dataset.id}/pipeline`)
: push(`/datasets/${dataset.id}/documents`)
if (isExternalProvider)
push(`/datasets/${dataset.id}/hitTesting`)
else if (isPipelineUnpublished)
push(`/datasets/${dataset.id}/pipeline`)
else
push(`/datasets/${dataset.id}/documents`)
}}
>
{!dataset.embedding_available && (

View File

@ -0,0 +1,3 @@
const DatasetsLoading = () => null
export default DatasetsLoading

View File

@ -0,0 +1,3 @@
const DatasetPreview = () => null
export default DatasetPreview

View File

@ -39,7 +39,7 @@ const Collapse = ({
<div className='mx-1 mb-1 rounded-lg border-t border-divider-subtle bg-components-panel-on-panel-item-bg py-1'>
{
items.map(item => (
<div key={item.key} onClick={() => onSelect && onSelect(item)}>
<div key={item.key} onClick={() => onSelect?.(item)}>
{renderItem(item)}
</div>
))

View File

@ -49,7 +49,7 @@ const ModelLoadBalancingConfigs = ({
provider,
model,
configurationMethod,
currentCustomConfigurationModelFixedFields,
currentCustomConfigurationModelFixedFields: _currentCustomConfigurationModelFixedFields,
withSwitch = false,
className,
modelCredential,

View File

@ -33,7 +33,7 @@ type Props = {
}
const AppPicker: FC<Props> = ({
scope,
scope: _scope,
disabled,
trigger,
placement = 'right-start',
@ -90,7 +90,7 @@ const AppPicker: FC<Props> = ({
}
// Set up MutationObserver to watch DOM changes
mutationObserver = new MutationObserver((mutations) => {
mutationObserver = new MutationObserver((_mutations) => {
if (observerTarget.current) {
setupIntersectionObserver()
mutationObserver?.disconnect()

View File

@ -148,7 +148,7 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
})
}
}
catch (e) {
catch {
Toast.notify({ type: 'error', message: t('common.error') })
}
}

View File

@ -51,7 +51,7 @@ export const useFieldList = ({
const handleListSortChange = useCallback((list: SortableItem[]) => {
const newInputFields = list.map((item) => {
const { id, chosen, selected, ...filed } = item
const { id: _id, chosen: _chosen, selected: _selected, ...filed } = item
return filed
})
handleInputFieldsChange(newInputFields)

View File

@ -15,7 +15,8 @@ const Header = () => {
isPreparingDataSource,
setIsPreparingDataSource,
} = workflowStore.getState()
isPreparingDataSource && setIsPreparingDataSource?.(false)
if (isPreparingDataSource)
setIsPreparingDataSource?.(false)
handleCancelDebugAndPreviewPanel()
}, [workflowStore])

View File

@ -104,7 +104,7 @@ export const useNodesSyncDraft = () => {
const res = await syncWorkflowDraft(postParams)
setSyncWorkflowDraftHash(res.hash)
setDraftUpdatedAt(res.updated_at)
callback?.onSuccess && callback.onSuccess()
callback?.onSuccess?.()
}
catch (error: any) {
if (error && error.json && !error.bodyUsed) {
@ -113,10 +113,10 @@ export const useNodesSyncDraft = () => {
handleRefreshWorkflowDraft()
})
}
callback?.onError && callback.onError()
callback?.onError?.()
}
finally {
callback?.onSettled && callback.onSettled()
callback?.onSettled?.()
}
}
}, [getPostParams, getNodesReadOnly, workflowStore, handleRefreshWorkflowDraft])

View File

@ -363,7 +363,8 @@ const TextGeneration: FC<IMainProps> = ({
(async () => {
if (!appData || !appParams)
return
!isWorkflow && fetchSavedMessage()
if (!isWorkflow)
fetchSavedMessage()
const { app_id: appId, site: siteInfo, custom_config } = appData
setAppId(appId)
setSiteInfo(siteInfo as SiteInfo)

View File

@ -126,8 +126,8 @@ const Result: FC<IResultProps> = ({
let hasEmptyInput = ''
const requiredVars = prompt_variables?.filter(({ key, name, required, type }) => {
if(type === 'boolean')
return false // boolean input is not required
if(type === 'boolean' || type === 'checkbox')
return false // boolean/checkbox input is not required
const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
return res
}) || [] // compatible with old version

View File

@ -51,6 +51,8 @@ const RunOnce: FC<IRunOnceProps> = ({
promptConfig.prompt_variables.forEach((item) => {
if (item.type === 'string' || item.type === 'paragraph')
newInputs[item.key] = ''
else if (item.type === 'checkbox')
newInputs[item.key] = false
else
newInputs[item.key] = undefined
})
@ -77,6 +79,8 @@ const RunOnce: FC<IRunOnceProps> = ({
newInputs[item.key] = item.default || ''
else if (item.type === 'number')
newInputs[item.key] = item.default
else if (item.type === 'checkbox')
newInputs[item.key] = item.default || false
else if (item.type === 'file')
newInputs[item.key] = item.default
else if (item.type === 'file-list')
@ -96,7 +100,7 @@ const RunOnce: FC<IRunOnceProps> = ({
{(inputs === null || inputs === undefined || Object.keys(inputs).length === 0) || !isInitialized ? null
: promptConfig.prompt_variables.map(item => (
<div className='mt-4 w-full' key={item.key}>
{item.type !== 'boolean' && (
{item.type !== 'checkbox' && (
<label className='system-md-semibold flex h-6 items-center text-text-secondary'>{item.name}</label>
)}
<div className='mt-1'>
@ -134,7 +138,7 @@ const RunOnce: FC<IRunOnceProps> = ({
onChange={(e: ChangeEvent<HTMLInputElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
/>
)}
{item.type === 'boolean' && (
{item.type === 'checkbox' && (
<BoolInput
name={item.name || item.key}
value={!!inputs[item.key]}

View File

@ -62,8 +62,10 @@ const SwrInitializer = ({
return
}
if (searchParams.has('access_token') || searchParams.has('refresh_token')) {
consoleToken && localStorage.setItem('console_token', consoleToken)
refreshToken && localStorage.setItem('refresh_token', refreshToken)
if (consoleToken)
localStorage.setItem('console_token', consoleToken)
if (refreshToken)
localStorage.setItem('refresh_token', refreshToken)
const redirectUrl = resolvePostLoginRedirect(searchParams)
if (redirectUrl)
location.replace(redirectUrl)

View File

@ -161,8 +161,7 @@ export const useNodesSyncDraft = () => {
})
setSyncWorkflowDraftHash(res.hash)
setDraftUpdatedAt(res.updated_at)
console.log('Leader successfully synced workflow draft')
callback?.onSuccess && callback.onSuccess()
callback?.onSuccess?.()
}
catch (error: any) {
console.error('Leader failed to sync workflow draft:', error)
@ -174,10 +173,10 @@ export const useNodesSyncDraft = () => {
}
})
}
callback?.onError && callback.onError()
callback?.onError?.()
}
finally {
callback?.onSettled && callback.onSettled()
callback?.onSettled?.()
}
}
}, [workflowStore, getPostParams, getNodesReadOnly, handleRefreshWorkflowDraft])

View File

@ -8,7 +8,7 @@ import {
} from '@/app/components/workflow/types'
import {
useWorkflowInit,
} from './hooks'
} from './hooks/use-workflow-init'
import {
initialEdges,
initialNodes,

View File

@ -73,7 +73,7 @@ const Tool: FC<Props> = ({
if (isHovering && !isAllSelected) {
return (
<span className='system-xs-regular text-components-button-secondary-accent-text'
onClick={(e) => {
onClick={() => {
onSelectMultiple?.(BlockEnum.Tool, actions.filter(action => !getIsDisabled(action)).map((tool) => {
const params: Record<string, string> = {}
if (tool.parameters) {

View File

@ -108,7 +108,8 @@ export const useShortcuts = (): void => {
const { showDebugAndPreviewPanel } = workflowStore.getState()
if (shouldHandleShortcut(e) && !showDebugAndPreviewPanel) {
e.preventDefault()
workflowHistoryShortcutsEnabled && handleHistoryBack()
if (workflowHistoryShortcutsEnabled)
handleHistoryBack()
}
}, { exactMatch: true, useCapture: true })
@ -117,7 +118,8 @@ export const useShortcuts = (): void => {
(e) => {
if (shouldHandleShortcut(e)) {
e.preventDefault()
workflowHistoryShortcutsEnabled && handleHistoryForward()
if (workflowHistoryShortcutsEnabled)
handleHistoryForward()
}
},
{ exactMatch: true, useCapture: true },

View File

@ -16,23 +16,25 @@ import type { WorkflowHistoryEventMeta } from '../workflow-history-store'
* - InputChange events in Node Panels do not trigger state changes.
* - Resizing UI elements does not trigger state changes.
*/
export enum WorkflowHistoryEvent {
NodeTitleChange = 'NodeTitleChange',
NodeDescriptionChange = 'NodeDescriptionChange',
NodeDragStop = 'NodeDragStop',
NodeChange = 'NodeChange',
NodeConnect = 'NodeConnect',
NodePaste = 'NodePaste',
NodeDelete = 'NodeDelete',
EdgeDelete = 'EdgeDelete',
EdgeDeleteByDeleteBranch = 'EdgeDeleteByDeleteBranch',
NodeAdd = 'NodeAdd',
NodeResize = 'NodeResize',
NoteAdd = 'NoteAdd',
NoteChange = 'NoteChange',
NoteDelete = 'NoteDelete',
LayoutOrganize = 'LayoutOrganize',
}
export const WorkflowHistoryEvent = {
NodeTitleChange: 'NodeTitleChange',
NodeDescriptionChange: 'NodeDescriptionChange',
NodeDragStop: 'NodeDragStop',
NodeChange: 'NodeChange',
NodeConnect: 'NodeConnect',
NodePaste: 'NodePaste',
NodeDelete: 'NodeDelete',
EdgeDelete: 'EdgeDelete',
EdgeDeleteByDeleteBranch: 'EdgeDeleteByDeleteBranch',
NodeAdd: 'NodeAdd',
NodeResize: 'NodeResize',
NoteAdd: 'NoteAdd',
NoteChange: 'NoteChange',
NoteDelete: 'NoteDelete',
LayoutOrganize: 'LayoutOrganize',
} as const
export type WorkflowHistoryEventT = keyof typeof WorkflowHistoryEvent
export const useWorkflowHistory = () => {
const store = useStoreApi()
@ -65,7 +67,7 @@ export const useWorkflowHistory = () => {
// Some events may be triggered multiple times in a short period of time.
// We debounce the history state update to avoid creating multiple history states
// with minimal changes.
const saveStateToHistoryRef = useRef(debounce((event: WorkflowHistoryEvent, meta?: WorkflowHistoryEventMeta) => {
const saveStateToHistoryRef = useRef(debounce((event: WorkflowHistoryEventT, meta?: WorkflowHistoryEventMeta) => {
workflowHistoryStore.setState({
workflowHistoryEvent: event,
workflowHistoryEventMeta: meta,
@ -74,7 +76,7 @@ export const useWorkflowHistory = () => {
})
}, 500))
const saveStateToHistory = useCallback((event: WorkflowHistoryEvent, meta?: WorkflowHistoryEventMeta) => {
const saveStateToHistory = useCallback((event: WorkflowHistoryEventT, meta?: WorkflowHistoryEventMeta) => {
switch (event) {
case WorkflowHistoryEvent.NoteChange:
// Hint: Note change does not trigger when note text changes,
@ -105,7 +107,7 @@ export const useWorkflowHistory = () => {
}
}, [])
const getHistoryLabel = useCallback((event: WorkflowHistoryEvent) => {
const getHistoryLabel = useCallback((event: WorkflowHistoryEventT) => {
switch (event) {
case WorkflowHistoryEvent.NodeTitleChange:
return t('workflow.changeHistory.nodeTitleChange')

View File

@ -10,7 +10,7 @@ import {
NODE_LAYOUT_VERTICAL_PADDING,
WORKFLOW_DATA_UPDATE,
} from '../constants'
import type { Node, WorkflowDataUpdater } from '../types'
import type { WorkflowDataUpdater } from '../types'
import { BlockEnum, ControlMode } from '../types'
import {
getLayoutByDagre,
@ -18,6 +18,7 @@ import {
initialEdges,
initialNodes,
} from '../utils'
import type { LayoutResult } from '../utils'
import {
useNodesReadOnly,
useSelectionInteractions,
@ -111,10 +112,17 @@ export const useWorkflowOrganize = () => {
&& node.type === CUSTOM_NODE,
)
const childLayoutsMap: Record<string, any> = {}
loopAndIterationNodes.forEach((node) => {
childLayoutsMap[node.id] = getLayoutForChildNodes(node.id, nodes, edges)
})
const childLayoutEntries = await Promise.all(
loopAndIterationNodes.map(async node => [
node.id,
await getLayoutForChildNodes(node.id, nodes, edges),
] as const),
)
const childLayoutsMap = childLayoutEntries.reduce((acc, [nodeId, layout]) => {
if (layout)
acc[nodeId] = layout
return acc
}, {} as Record<string, LayoutResult>)
const containerSizeChanges: Record<string, { width: number, height: number }> = {}
@ -122,37 +130,20 @@ export const useWorkflowOrganize = () => {
const childLayout = childLayoutsMap[parentNode.id]
if (!childLayout) return
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity
let hasChildren = false
const {
bounds,
nodes: layoutNodes,
} = childLayout
const childNodes = nodes.filter(node => node.parentId === parentNode.id)
if (!layoutNodes.size)
return
childNodes.forEach((node) => {
if (childLayout.node(node.id)) {
hasChildren = true
const childNodeWithPosition = childLayout.node(node.id)
const requiredWidth = (bounds.maxX - bounds.minX) + NODE_LAYOUT_HORIZONTAL_PADDING * 2
const requiredHeight = (bounds.maxY - bounds.minY) + NODE_LAYOUT_VERTICAL_PADDING * 2
const nodeX = childNodeWithPosition.x - node.width! / 2
const nodeY = childNodeWithPosition.y - node.height! / 2
minX = Math.min(minX, nodeX)
minY = Math.min(minY, nodeY)
maxX = Math.max(maxX, nodeX + node.width!)
maxY = Math.max(maxY, nodeY + node.height!)
}
})
if (hasChildren) {
const requiredWidth = maxX - minX + NODE_LAYOUT_HORIZONTAL_PADDING * 2
const requiredHeight = maxY - minY + NODE_LAYOUT_VERTICAL_PADDING * 2
containerSizeChanges[parentNode.id] = {
width: Math.max(parentNode.width || 0, requiredWidth),
height: Math.max(parentNode.height || 0, requiredHeight),
}
containerSizeChanges[parentNode.id] = {
width: Math.max(parentNode.width || 0, requiredWidth),
height: Math.max(parentNode.height || 0, requiredHeight),
}
})
@ -175,63 +166,65 @@ export const useWorkflowOrganize = () => {
})
})
const layout = getLayoutByDagre(nodesWithUpdatedSizes, edges)
const layout = await getLayoutByDagre(nodesWithUpdatedSizes, edges)
const rankMap = {} as Record<string, Node>
nodesWithUpdatedSizes.forEach((node) => {
if (!node.parentId && node.type === CUSTOM_NODE) {
const rank = layout.node(node.id).rank!
if (!rankMap[rank]) {
rankMap[rank] = node
}
else {
if (rankMap[rank].position.y > node.position.y)
rankMap[rank] = node
// Build layer map for vertical alignment - nodes in the same layer should align
const layerMap = new Map<number, { minY: number; maxHeight: number }>()
layout.nodes.forEach((layoutInfo) => {
if (layoutInfo.layer !== undefined) {
const existing = layerMap.get(layoutInfo.layer)
const newLayerInfo = {
minY: existing ? Math.min(existing.minY, layoutInfo.y) : layoutInfo.y,
maxHeight: existing ? Math.max(existing.maxHeight, layoutInfo.height) : layoutInfo.height,
}
layerMap.set(layoutInfo.layer, newLayerInfo)
}
})
const newNodes = produce(nodesWithUpdatedSizes, (draft) => {
draft.forEach((node) => {
if (!node.parentId && node.type === CUSTOM_NODE) {
const nodeWithPosition = layout.node(node.id)
const layoutInfo = layout.nodes.get(node.id)
if (!layoutInfo)
return
// Calculate vertical position with layer alignment
let yPosition = layoutInfo.y
if (layoutInfo.layer !== undefined) {
const layerInfo = layerMap.get(layoutInfo.layer)
if (layerInfo) {
// Align to the center of the tallest node in this layer
const layerCenterY = layerInfo.minY + layerInfo.maxHeight / 2
yPosition = layerCenterY - layoutInfo.height / 2
}
}
node.position = {
x: nodeWithPosition.x - node.width! / 2,
y: nodeWithPosition.y - node.height! / 2 + rankMap[nodeWithPosition.rank!].height! / 2,
x: layoutInfo.x,
y: yPosition,
}
}
})
loopAndIterationNodes.forEach((parentNode) => {
const childLayout = childLayoutsMap[parentNode.id]
if (!childLayout) return
if (!childLayout)
return
const childNodes = draft.filter(node => node.parentId === parentNode.id)
const {
bounds,
nodes: layoutNodes,
} = childLayout
let minX = Infinity
let minY = Infinity
childNodes.forEach((childNode) => {
const layoutInfo = layoutNodes.get(childNode.id)
if (!layoutInfo)
return
childNodes.forEach((node) => {
if (childLayout.node(node.id)) {
const childNodeWithPosition = childLayout.node(node.id)
const nodeX = childNodeWithPosition.x - node.width! / 2
const nodeY = childNodeWithPosition.y - node.height! / 2
minX = Math.min(minX, nodeX)
minY = Math.min(minY, nodeY)
}
})
childNodes.forEach((node) => {
if (childLayout.node(node.id)) {
const childNodeWithPosition = childLayout.node(node.id)
node.position = {
x: NODE_LAYOUT_HORIZONTAL_PADDING + (childNodeWithPosition.x - node.width! / 2 - minX),
y: NODE_LAYOUT_VERTICAL_PADDING + (childNodeWithPosition.y - node.height! / 2 - minY),
}
childNode.position = {
x: NODE_LAYOUT_HORIZONTAL_PADDING + (layoutInfo.x - bounds.minX),
y: NODE_LAYOUT_VERTICAL_PADDING + (layoutInfo.y - bounds.minY),
}
})
})

View File

@ -5,6 +5,7 @@ import { uniqBy } from 'lodash-es'
import {
getIncomers,
getOutgoers,
useStoreApi,
} from 'reactflow'
import type {
Connection,
@ -53,6 +54,7 @@ export const useWorkflow = () => {
const collaborativeWorkflow = useCollaborativeWorkflow()
const { getAvailableBlocks } = useAvailableBlocks()
const { nodesMap } = useNodesMetaData()
const store = useStoreApi()
const getNodeById = useCallback((nodeId: string) => {
const { nodes } = collaborativeWorkflow.getState()
@ -322,8 +324,12 @@ export const useWorkflow = () => {
return startNodes
}, [nodesMap, getRootNodesById])
const isValidConnection = useCallback(({ source, sourceHandle, target }: Connection) => {
const { nodes, edges } = collaborativeWorkflow.getState()
const isValidConnection = useCallback(({ source, sourceHandle: _sourceHandle, target }: Connection) => {
const {
edges,
getNodes,
} = store.getState()
const nodes = getNodes()
const sourceNode: Node = nodes.find(node => node.id === source)!
const targetNode: Node = nodes.find(node => node.id === target)!

View File

@ -407,7 +407,10 @@ const VarReferencePicker: FC<Props> = ({
<WrapElem onClick={() => {
if (readonly)
return
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
if (!isConstant)
setOpen(!open)
else
setControlFocus(Date.now())
}} className='group/picker-trigger-wrap relative !flex'>
<>
{isAddBtnTrigger
@ -457,7 +460,10 @@ const VarReferencePicker: FC<Props> = ({
onClick={() => {
if (readonly)
return
!isConstant ? setOpen(!open) : setControlFocus(Date.now())
if (!isConstant)
setOpen(!open)
else
setControlFocus(Date.now())
}}
className='h-full grow'
>

View File

@ -137,7 +137,7 @@ const Item: FC<ItemProps> = ({
const isHovering = isItemHovering || isChildrenHovering
const open = (isObj || isStructureOutput) && isHovering
useEffect(() => {
onHovering && onHovering(isHovering)
onHovering?.(isHovering)
}, [isHovering])
const handleChosen = (e: React.MouseEvent) => {
e.stopPropagation()

View File

@ -25,12 +25,12 @@ type Props = {
} & Partial<ResultPanelProps>
const LastRun: FC<Props> = ({
appId,
appId: _appId,
nodeId,
canSingleRun,
isRunAfterSingleRun,
updateNodeRunningStatus,
nodeInfo,
nodeInfo: _nodeInfo,
runningStatus: oneStepRunRunningStatus,
onSingleRunClicked,
singleRunResult,

View File

@ -88,7 +88,8 @@ const OptionCard = memo(({
)}
onClick={(e) => {
e.stopPropagation()
!readonly && enableSelect && id && onClick?.(id)
if (!readonly && enableSelect && id)
onClick?.(id)
}}
>
<div className={cn(

View File

@ -139,8 +139,10 @@ const JsonSchemaConfig: FC<JsonSchemaConfigProps> = ({
const handleResetDefaults = useCallback(() => {
if (currentTab === SchemaView.VisualEditor) {
setHoveringProperty(null)
advancedEditing && setAdvancedEditing(false)
isAddingNewField && setIsAddingNewField(false)
if (advancedEditing)
setAdvancedEditing(false)
if (isAddingNewField)
setIsAddingNewField(false)
}
setJsonSchema(DEFAULT_SCHEMA)
setJson(JSON.stringify(DEFAULT_SCHEMA, null, 2))

View File

@ -87,8 +87,10 @@ const EditCard: FC<EditCardProps> = ({
})
useSubscribe('fieldChangeSuccess', () => {
isAddingNewField && setIsAddingNewField(false)
advancedEditing && setAdvancedEditing(false)
if (isAddingNewField)
setIsAddingNewField(false)
if (advancedEditing)
setAdvancedEditing(false)
})
const emitPropertyNameChange = useCallback(() => {

View File

@ -45,8 +45,10 @@ export const useSchemaNodeOperations = (props: VisualEditorProps) => {
onChange(backupSchema)
setBackupSchema(null)
}
isAddingNewField && setIsAddingNewField(false)
advancedEditing && setAdvancedEditing(false)
if (isAddingNewField)
setIsAddingNewField(false)
if (advancedEditing)
setAdvancedEditing(false)
setHoveringProperty(null)
})
@ -221,7 +223,8 @@ export const useSchemaNodeOperations = (props: VisualEditorProps) => {
})
useSubscribe('addField', (params) => {
advancedEditing && setAdvancedEditing(false)
if (advancedEditing)
setAdvancedEditing(false)
setBackupSchema(jsonSchema)
const { path } = params as AddEventParams
setIsAddingNewField(true)

View File

@ -293,6 +293,11 @@ const Panel: FC<NodePanelProps<LLMNodeType>> = ({
type='string'
description={t(`${i18nPrefix}.outputVars.output`)}
/>
<VarItem
name='reasoning_content'
type='string'
description={t(`${i18nPrefix}.outputVars.reasoning_content`)}
/>
<VarItem
name='usage'
type='object'

View File

@ -22,7 +22,7 @@ type ConditionValueProps = {
}
const ConditionValue = ({
variableSelector,
labelName,
labelName: _labelName,
operator,
value,
}: ConditionValueProps) => {

View File

@ -35,7 +35,8 @@ const VariableModalTrigger = ({
open={open}
onOpenChange={() => {
setOpen(v => !v)
open && onClose()
if (open)
onClose()
}}
placement='left-start'
offset={{
@ -45,7 +46,8 @@ const VariableModalTrigger = ({
>
<PortalToFollowElemTrigger onClick={() => {
setOpen(v => !v)
open && onClose()
if (open)
onClose()
}}>
<Button variant='primary'>
<RiAddLine className='mr-1 h-4 w-4' />

View File

@ -33,7 +33,8 @@ const VariableTrigger = ({
open={open}
onOpenChange={() => {
setOpen(v => !v)
open && onClose()
if (open)
onClose()
}}
placement='left-start'
offset={{
@ -43,7 +44,8 @@ const VariableTrigger = ({
>
<PortalToFollowElemTrigger onClick={() => {
setOpen(v => !v)
open && onClose()
if (open)
onClose()
}}>
<Button variant='primary'>
<RiAddLine className='mr-1 h-4 w-4' />

View File

@ -81,9 +81,12 @@ const RunPanel: FC<RunProps> = ({
const switchTab = async (tab: string) => {
setCurrentTab(tab)
if (tab === 'RESULT')
runDetailUrl && await getResult()
tracingListUrl && await getTracingList()
if (tab === 'RESULT') {
if (runDetailUrl)
await getResult()
}
if (tracingListUrl)
await getTracingList()
}
useEffect(() => {

View File

@ -1,246 +0,0 @@
import dagre from '@dagrejs/dagre'
import {
cloneDeep,
} from 'lodash-es'
import type {
Edge,
Node,
} from '../types'
import {
BlockEnum,
} from '../types'
import {
CUSTOM_NODE,
NODE_LAYOUT_HORIZONTAL_PADDING,
NODE_LAYOUT_MIN_DISTANCE,
NODE_LAYOUT_VERTICAL_PADDING,
} from '../constants'
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
export const getLayoutByDagre = (originNodes: Node[], originEdges: Edge[]) => {
const dagreGraph = new dagre.graphlib.Graph({ compound: true })
dagreGraph.setDefaultEdgeLabel(() => ({}))
const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE)
const edges = cloneDeep(originEdges).filter(edge => (!edge.data?.isInIteration && !edge.data?.isInLoop))
// The default dagre layout algorithm often fails to correctly order the branches
// of an If/Else node, leading to crossed edges.
//
// To solve this, we employ a "virtual container" strategy:
// 1. A virtual, compound parent node (the "container") is created for each If/Else node's branches.
// 2. Each direct child of the If/Else node is preceded by a virtual dummy node. These dummies are placed inside the container.
// 3. A rigid, sequential chain of invisible edges is created between these dummy nodes (e.g., dummy_IF -> dummy_ELIF -> dummy_ELSE).
//
// This forces dagre to treat the ordered branches as an unbreakable, atomic group,
// ensuring their layout respects the intended logical sequence.
const ifElseNodes = nodes.filter(node => node.data.type === BlockEnum.IfElse)
let virtualLogicApplied = false
ifElseNodes.forEach((ifElseNode) => {
const childEdges = edges.filter(e => e.source === ifElseNode.id)
if (childEdges.length <= 1)
return
virtualLogicApplied = true
const sortedChildEdges = childEdges.sort((edgeA, edgeB) => {
const handleA = edgeA.sourceHandle
const handleB = edgeB.sourceHandle
if (handleA && handleB) {
const cases = (ifElseNode.data as any).cases || []
const isAElse = handleA === 'false'
const isBElse = handleB === 'false'
if (isAElse) return 1
if (isBElse) return -1
const indexA = cases.findIndex((c: any) => c.case_id === handleA)
const indexB = cases.findIndex((c: any) => c.case_id === handleB)
if (indexA !== -1 && indexB !== -1)
return indexA - indexB
}
return 0
})
const parentDummyId = `dummy-parent-${ifElseNode.id}`
dagreGraph.setNode(parentDummyId, { width: 1, height: 1 })
const dummyNodes: string[] = []
sortedChildEdges.forEach((edge) => {
const dummyNodeId = `dummy-${edge.source}-${edge.target}`
dummyNodes.push(dummyNodeId)
dagreGraph.setNode(dummyNodeId, { width: 1, height: 1 })
dagreGraph.setParent(dummyNodeId, parentDummyId)
const edgeIndex = edges.findIndex(e => e.id === edge.id)
if (edgeIndex > -1)
edges.splice(edgeIndex, 1)
edges.push({ id: `e-${edge.source}-${dummyNodeId}`, source: edge.source, target: dummyNodeId, sourceHandle: edge.sourceHandle } as Edge)
edges.push({ id: `e-${dummyNodeId}-${edge.target}`, source: dummyNodeId, target: edge.target, targetHandle: edge.targetHandle } as Edge)
})
for (let i = 0; i < dummyNodes.length - 1; i++) {
const sourceDummy = dummyNodes[i]
const targetDummy = dummyNodes[i + 1]
edges.push({ id: `e-dummy-${sourceDummy}-${targetDummy}`, source: sourceDummy, target: targetDummy } as Edge)
}
})
dagreGraph.setGraph({
rankdir: 'LR',
align: 'UL',
nodesep: 40,
ranksep: virtualLogicApplied ? 30 : 60,
ranker: 'tight-tree',
marginx: 30,
marginy: 200,
})
nodes.forEach((node) => {
dagreGraph.setNode(node.id, {
width: node.width!,
height: node.height!,
})
})
edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target)
})
dagre.layout(dagreGraph)
return dagreGraph
}
export const getLayoutForChildNodes = (parentNodeId: string, originNodes: Node[], originEdges: Edge[]) => {
const dagreGraph = new dagre.graphlib.Graph()
dagreGraph.setDefaultEdgeLabel(() => ({}))
const nodes = cloneDeep(originNodes).filter(node => node.parentId === parentNodeId)
const edges = cloneDeep(originEdges).filter(edge =>
(edge.data?.isInIteration && edge.data?.iteration_id === parentNodeId)
|| (edge.data?.isInLoop && edge.data?.loop_id === parentNodeId),
)
const startNode = nodes.find(node =>
node.type === CUSTOM_ITERATION_START_NODE
|| node.type === CUSTOM_LOOP_START_NODE
|| node.data?.type === BlockEnum.LoopStart
|| node.data?.type === BlockEnum.IterationStart,
)
if (!startNode) {
dagreGraph.setGraph({
rankdir: 'LR',
align: 'UL',
nodesep: 40,
ranksep: 60,
marginx: NODE_LAYOUT_HORIZONTAL_PADDING,
marginy: NODE_LAYOUT_VERTICAL_PADDING,
})
nodes.forEach((node) => {
dagreGraph.setNode(node.id, {
width: node.width || 244,
height: node.height || 100,
})
})
edges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target)
})
dagre.layout(dagreGraph)
return dagreGraph
}
const startNodeOutEdges = edges.filter(edge => edge.source === startNode.id)
const firstConnectedNodes = startNodeOutEdges.map(edge =>
nodes.find(node => node.id === edge.target),
).filter(Boolean) as Node[]
const nonStartNodes = nodes.filter(node => node.id !== startNode.id)
const nonStartEdges = edges.filter(edge => edge.source !== startNode.id && edge.target !== startNode.id)
dagreGraph.setGraph({
rankdir: 'LR',
align: 'UL',
nodesep: 40,
ranksep: 60,
marginx: NODE_LAYOUT_HORIZONTAL_PADDING / 2,
marginy: NODE_LAYOUT_VERTICAL_PADDING / 2,
})
nonStartNodes.forEach((node) => {
dagreGraph.setNode(node.id, {
width: node.width || 244,
height: node.height || 100,
})
})
nonStartEdges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target)
})
dagre.layout(dagreGraph)
const startNodeSize = {
width: startNode.width || 44,
height: startNode.height || 48,
}
const startNodeX = NODE_LAYOUT_HORIZONTAL_PADDING / 1.5
let startNodeY = 100
let minFirstLayerX = Infinity
let avgFirstLayerY = 0
let firstLayerCount = 0
if (firstConnectedNodes.length > 0) {
firstConnectedNodes.forEach((node) => {
if (dagreGraph.node(node.id)) {
const nodePos = dagreGraph.node(node.id)
avgFirstLayerY += nodePos.y
firstLayerCount++
minFirstLayerX = Math.min(minFirstLayerX, nodePos.x - nodePos.width / 2)
}
})
if (firstLayerCount > 0) {
avgFirstLayerY /= firstLayerCount
startNodeY = avgFirstLayerY
}
const minRequiredX = startNodeX + startNodeSize.width + NODE_LAYOUT_MIN_DISTANCE
if (minFirstLayerX < minRequiredX) {
const shiftX = minRequiredX - minFirstLayerX
nonStartNodes.forEach((node) => {
if (dagreGraph.node(node.id)) {
const nodePos = dagreGraph.node(node.id)
dagreGraph.setNode(node.id, {
x: nodePos.x + shiftX,
y: nodePos.y,
width: nodePos.width,
height: nodePos.height,
})
}
})
}
}
dagreGraph.setNode(startNode.id, {
x: startNodeX + startNodeSize.width / 2,
y: startNodeY,
width: startNodeSize.width,
height: startNodeSize.height,
})
startNodeOutEdges.forEach((edge) => {
dagreGraph.setEdge(edge.source, edge.target)
})
return dagreGraph
}

View File

@ -1,7 +1,7 @@
export * from './node'
export * from './edge'
export * from './workflow-init'
export * from './dagre-layout'
export * from './layout'
export * from './common'
export * from './tool'
export * from './workflow'

View File

@ -0,0 +1,529 @@
import ELK from 'elkjs/lib/elk.bundled.js'
import type { ElkNode, LayoutOptions } from 'elkjs/lib/elk-api'
import { cloneDeep } from 'lodash-es'
import type {
Edge,
Node,
} from '../types'
import {
BlockEnum,
} from '../types'
import {
CUSTOM_NODE,
NODE_LAYOUT_HORIZONTAL_PADDING,
NODE_LAYOUT_VERTICAL_PADDING,
} from '../constants'
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
import type { CaseItem, IfElseNodeType } from '../nodes/if-else/types'
// Although the file name refers to Dagre, the implementation now relies on ELK's layered algorithm.
// Keep the export signatures unchanged to minimise the blast radius while we migrate the layout stack.
const elk = new ELK()
const DEFAULT_NODE_WIDTH = 244
const DEFAULT_NODE_HEIGHT = 100
const ROOT_LAYOUT_OPTIONS = {
'elk.algorithm': 'layered',
'elk.direction': 'RIGHT',
// === Spacing - Maximum spacing to prevent any overlap ===
'elk.layered.spacing.nodeNodeBetweenLayers': '100',
'elk.spacing.nodeNode': '80',
'elk.spacing.edgeNode': '50',
'elk.spacing.edgeEdge': '30',
'elk.spacing.edgeLabel': '10',
'elk.spacing.portPort': '20',
// === Port Configuration ===
'elk.portConstraints': 'FIXED_ORDER',
'elk.layered.considerModelOrder.strategy': 'PREFER_EDGES',
'elk.port.side': 'SOUTH',
// === Node Placement - Best quality ===
'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX',
'elk.layered.nodePlacement.favorStraightEdges': 'true',
'elk.layered.nodePlacement.linearSegments.deflectionDampening': '0.5',
'elk.layered.nodePlacement.networkSimplex.nodeFlexibility': 'NODE_SIZE',
// === Edge Routing - Maximum quality ===
'elk.edgeRouting': 'SPLINES',
'elk.layered.edgeRouting.selfLoopPlacement': 'NORTH',
'elk.layered.edgeRouting.sloppySplineRouting': 'false',
'elk.layered.edgeRouting.splines.mode': 'CONSERVATIVE',
'elk.layered.edgeRouting.splines.sloppy.layerSpacingFactor': '1.2',
// === Crossing Minimization - Most aggressive ===
'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP',
'elk.layered.crossingMinimization.greedySwitch.type': 'TWO_SIDED',
'elk.layered.crossingMinimization.greedySwitchHierarchical.type': 'TWO_SIDED',
'elk.layered.crossingMinimization.semiInteractive': 'true',
'elk.layered.crossingMinimization.hierarchicalSweepiness': '0.9',
// === Layering Strategy - Best quality ===
'elk.layered.layering.strategy': 'NETWORK_SIMPLEX',
'elk.layered.layering.networkSimplex.nodeFlexibility': 'NODE_SIZE',
'elk.layered.layering.layerConstraint': 'NONE',
'elk.layered.layering.minWidth.upperBoundOnWidth': '4',
// === Cycle Breaking ===
'elk.layered.cycleBreaking.strategy': 'DEPTH_FIRST',
// === Connected Components ===
'elk.separateConnectedComponents': 'true',
'elk.spacing.componentComponent': '100',
// === Node Size Constraints ===
'elk.nodeSize.constraints': 'NODE_LABELS',
'elk.nodeSize.options': 'DEFAULT_MINIMUM_SIZE MINIMUM_SIZE_ACCOUNTS_FOR_PADDING',
// === Edge Label Placement ===
'elk.edgeLabels.placement': 'CENTER',
'elk.edgeLabels.inline': 'true',
// === Compaction ===
'elk.layered.compaction.postCompaction.strategy': 'EDGE_LENGTH',
'elk.layered.compaction.postCompaction.constraints': 'EDGE_LENGTH',
// === High-Quality Mode ===
'elk.layered.thoroughness': '10',
'elk.layered.wrapping.strategy': 'OFF',
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
// === Additional Optimizations ===
'elk.layered.feedbackEdges': 'true',
'elk.layered.mergeEdges': 'false',
'elk.layered.mergeHierarchyEdges': 'false',
'elk.layered.allowNonFlowPortsToSwitchSides': 'false',
'elk.layered.northOrSouthPort': 'false',
'elk.partitioning.activate': 'false',
'elk.junctionPoints': 'true',
// === Content Alignment ===
'elk.contentAlignment': 'V_TOP H_LEFT',
'elk.alignment': 'AUTOMATIC',
}
const CHILD_LAYOUT_OPTIONS = {
'elk.algorithm': 'layered',
'elk.direction': 'RIGHT',
// === Spacing - High quality for child nodes ===
'elk.layered.spacing.nodeNodeBetweenLayers': '80',
'elk.spacing.nodeNode': '60',
'elk.spacing.edgeNode': '40',
'elk.spacing.edgeEdge': '25',
'elk.spacing.edgeLabel': '8',
'elk.spacing.portPort': '15',
// === Node Placement - Best quality ===
'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX',
'elk.layered.nodePlacement.favorStraightEdges': 'true',
'elk.layered.nodePlacement.linearSegments.deflectionDampening': '0.5',
'elk.layered.nodePlacement.networkSimplex.nodeFlexibility': 'NODE_SIZE',
// === Edge Routing - Maximum quality ===
'elk.edgeRouting': 'SPLINES',
'elk.layered.edgeRouting.sloppySplineRouting': 'false',
'elk.layered.edgeRouting.splines.mode': 'CONSERVATIVE',
// === Crossing Minimization - Aggressive ===
'elk.layered.crossingMinimization.strategy': 'LAYER_SWEEP',
'elk.layered.crossingMinimization.greedySwitch.type': 'TWO_SIDED',
'elk.layered.crossingMinimization.semiInteractive': 'true',
// === Layering Strategy ===
'elk.layered.layering.strategy': 'NETWORK_SIMPLEX',
'elk.layered.layering.networkSimplex.nodeFlexibility': 'NODE_SIZE',
// === Cycle Breaking ===
'elk.layered.cycleBreaking.strategy': 'DEPTH_FIRST',
// === Node Size ===
'elk.nodeSize.constraints': 'NODE_LABELS',
// === Compaction ===
'elk.layered.compaction.postCompaction.strategy': 'EDGE_LENGTH',
// === High-Quality Mode ===
'elk.layered.thoroughness': '10',
'elk.hierarchyHandling': 'INCLUDE_CHILDREN',
// === Additional Optimizations ===
'elk.layered.feedbackEdges': 'true',
'elk.layered.mergeEdges': 'false',
'elk.junctionPoints': 'true',
}
type LayoutInfo = {
x: number
y: number
width: number
height: number
layer?: number
}
type LayoutBounds = {
minX: number
minY: number
maxX: number
maxY: number
}
export type LayoutResult = {
nodes: Map<string, LayoutInfo>
bounds: LayoutBounds
}
// ELK Port definition for native port support
type ElkPortShape = {
id: string
layoutOptions?: LayoutOptions
}
type ElkNodeShape = {
id: string
width: number
height: number
ports?: ElkPortShape[]
layoutOptions?: LayoutOptions
children?: ElkNodeShape[]
}
type ElkEdgeShape = {
id: string
sources: string[]
targets: string[]
sourcePort?: string
targetPort?: string
}
const toElkNode = (node: Node): ElkNodeShape => ({
id: node.id,
width: node.width ?? DEFAULT_NODE_WIDTH,
height: node.height ?? DEFAULT_NODE_HEIGHT,
})
let edgeCounter = 0
const nextEdgeId = () => `elk-edge-${edgeCounter++}`
const createEdge = (
source: string,
target: string,
sourcePort?: string,
targetPort?: string,
): ElkEdgeShape => ({
id: nextEdgeId(),
sources: [source],
targets: [target],
sourcePort,
targetPort,
})
const collectLayout = (graph: ElkNode, predicate: (id: string) => boolean): LayoutResult => {
const result = new Map<string, LayoutInfo>()
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity
const visit = (node: ElkNode) => {
node.children?.forEach((child: ElkNode) => {
if (predicate(child.id)) {
const x = child.x ?? 0
const y = child.y ?? 0
const width = child.width ?? DEFAULT_NODE_WIDTH
const height = child.height ?? DEFAULT_NODE_HEIGHT
const layer = child?.layoutOptions?.['org.eclipse.elk.layered.layerIndex']
result.set(child.id, {
x,
y,
width,
height,
layer: layer ? Number.parseInt(layer) : undefined,
})
minX = Math.min(minX, x)
minY = Math.min(minY, y)
maxX = Math.max(maxX, x + width)
maxY = Math.max(maxY, y + height)
}
if (child.children?.length)
visit(child)
})
}
visit(graph)
if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
minX = 0
minY = 0
maxX = 0
maxY = 0
}
return {
nodes: result,
bounds: {
minX,
minY,
maxX,
maxY,
},
}
}
/**
* Build If/Else node with ELK native Ports instead of dummy nodes
* This is the recommended approach for handling multiple branches
*/
const buildIfElseWithPorts = (
ifElseNode: Node,
edges: Edge[],
): { node: ElkNodeShape; portMap: Map<string, string> } | null => {
const childEdges = edges.filter(edge => edge.source === ifElseNode.id)
if (childEdges.length <= 1)
return null
// Sort child edges according to case order
const sortedChildEdges = [...childEdges].sort((edgeA, edgeB) => {
const handleA = edgeA.sourceHandle
const handleB = edgeB.sourceHandle
if (handleA && handleB) {
const cases = (ifElseNode.data as IfElseNodeType).cases || []
const isAElse = handleA === 'false'
const isBElse = handleB === 'false'
if (isAElse)
return 1
if (isBElse)
return -1
const indexA = cases.findIndex((c: CaseItem) => c.case_id === handleA)
const indexB = cases.findIndex((c: CaseItem) => c.case_id === handleB)
if (indexA !== -1 && indexB !== -1)
return indexA - indexB
}
return 0
})
// Create ELK ports for each branch
const ports: ElkPortShape[] = sortedChildEdges.map((edge, index) => ({
id: `${ifElseNode.id}-port-${edge.sourceHandle || index}`,
layoutOptions: {
'port.side': 'EAST', // Ports on the right side (matching 'RIGHT' direction)
'port.index': String(index),
},
}))
// Build port mapping: sourceHandle -> portId
const portMap = new Map<string, string>()
sortedChildEdges.forEach((edge, index) => {
const portId = `${ifElseNode.id}-port-${edge.sourceHandle || index}`
portMap.set(edge.id, portId)
})
return {
node: {
id: ifElseNode.id,
width: ifElseNode.width ?? DEFAULT_NODE_WIDTH,
height: ifElseNode.height ?? DEFAULT_NODE_HEIGHT,
ports,
layoutOptions: {
'elk.portConstraints': 'FIXED_ORDER',
},
},
portMap,
}
}
const normaliseBounds = (layout: LayoutResult): LayoutResult => {
const {
nodes,
bounds,
} = layout
if (nodes.size === 0)
return layout
const offsetX = bounds.minX
const offsetY = bounds.minY
const adjustedNodes = new Map<string, LayoutInfo>()
nodes.forEach((info, id) => {
adjustedNodes.set(id, {
...info,
x: info.x - offsetX,
y: info.y - offsetY,
})
})
return {
nodes: adjustedNodes,
bounds: {
minX: 0,
minY: 0,
maxX: bounds.maxX - offsetX,
maxY: bounds.maxY - offsetY,
},
}
}
export const getLayoutByDagre = async (originNodes: Node[], originEdges: Edge[]): Promise<LayoutResult> => {
edgeCounter = 0
const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE)
const edges = cloneDeep(originEdges).filter(edge => (!edge.data?.isInIteration && !edge.data?.isInLoop))
const elkNodes: ElkNodeShape[] = []
const elkEdges: ElkEdgeShape[] = []
// Track which edges have been processed for If/Else nodes with ports
const edgeToPortMap = new Map<string, string>()
// Build nodes with ports for If/Else nodes
nodes.forEach((node) => {
if (node.data.type === BlockEnum.IfElse) {
const portsResult = buildIfElseWithPorts(node, edges)
if (portsResult) {
// Use node with ports
elkNodes.push(portsResult.node)
// Store port mappings for edges
portsResult.portMap.forEach((portId, edgeId) => {
edgeToPortMap.set(edgeId, portId)
})
}
else {
// No multiple branches, use normal node
elkNodes.push(toElkNode(node))
}
}
else {
elkNodes.push(toElkNode(node))
}
})
// Build edges with port connections
edges.forEach((edge) => {
const sourcePort = edgeToPortMap.get(edge.id)
elkEdges.push(createEdge(edge.source, edge.target, sourcePort))
})
const graph = {
id: 'workflow-root',
layoutOptions: ROOT_LAYOUT_OPTIONS,
children: elkNodes,
edges: elkEdges,
}
const layoutedGraph = await elk.layout(graph)
// No need to filter dummy nodes anymore, as we're using ports
const layout = collectLayout(layoutedGraph, () => true)
return normaliseBounds(layout)
}
const normaliseChildLayout = (
layout: LayoutResult,
nodes: Node[],
): LayoutResult => {
const result = new Map<string, LayoutInfo>()
layout.nodes.forEach((info, id) => {
result.set(id, info)
})
// Ensure iteration / loop start nodes do not collapse into the children.
const startNode = nodes.find(node =>
node.type === CUSTOM_ITERATION_START_NODE
|| node.type === CUSTOM_LOOP_START_NODE
|| node.data?.type === BlockEnum.LoopStart
|| node.data?.type === BlockEnum.IterationStart,
)
if (startNode) {
const startLayout = result.get(startNode.id)
if (startLayout) {
const desiredMinX = NODE_LAYOUT_HORIZONTAL_PADDING / 1.5
if (startLayout.x > desiredMinX) {
const shiftX = startLayout.x - desiredMinX
result.forEach((value, key) => {
result.set(key, {
...value,
x: value.x - shiftX,
})
})
}
const desiredMinY = startLayout.y
const deltaY = NODE_LAYOUT_VERTICAL_PADDING / 2
result.forEach((value, key) => {
result.set(key, {
...value,
y: value.y - desiredMinY + deltaY,
})
})
}
}
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity
result.forEach((value) => {
minX = Math.min(minX, value.x)
minY = Math.min(minY, value.y)
maxX = Math.max(maxX, value.x + value.width)
maxY = Math.max(maxY, value.y + value.height)
})
if (!Number.isFinite(minX) || !Number.isFinite(minY))
return layout
return normaliseBounds({
nodes: result,
bounds: {
minX,
minY,
maxX,
maxY,
},
})
}
export const getLayoutForChildNodes = async (
parentNodeId: string,
originNodes: Node[],
originEdges: Edge[],
): Promise<LayoutResult | null> => {
edgeCounter = 0
const nodes = cloneDeep(originNodes).filter(node => node.parentId === parentNodeId)
if (!nodes.length)
return null
const edges = cloneDeep(originEdges).filter(edge =>
(edge.data?.isInIteration && edge.data?.iteration_id === parentNodeId)
|| (edge.data?.isInLoop && edge.data?.loop_id === parentNodeId),
)
const elkNodes: ElkNodeShape[] = nodes.map(toElkNode)
const elkEdges: ElkEdgeShape[] = edges.map(edge => createEdge(edge.source, edge.target))
const graph = {
id: parentNodeId,
layoutOptions: CHILD_LAYOUT_OPTIONS,
children: elkNodes,
edges: elkEdges,
}
const layoutedGraph = await elk.layout(graph)
const layout = collectLayout(layoutedGraph, () => true)
return normaliseChildLayout(layout, nodes)
}

View File

@ -3,7 +3,7 @@ import { type StoreApi, create } from 'zustand'
import { type TemporalState, temporal } from 'zundo'
import isDeepEqual from 'fast-deep-equal'
import type { Edge, Node } from './types'
import type { WorkflowHistoryEvent } from './hooks'
import type { WorkflowHistoryEventT } from './hooks'
import { noop } from 'lodash-es'
export const WorkflowHistoryStoreContext = createContext<WorkflowHistoryStoreContextType>({ store: null, shortcutsEnabled: true, setShortcutsEnabled: noop })
@ -98,7 +98,7 @@ function createStore({
export type WorkflowHistoryStore = {
nodes: Node[]
edges: Edge[]
workflowHistoryEvent: WorkflowHistoryEvent | undefined
workflowHistoryEvent: WorkflowHistoryEventT | undefined
workflowHistoryEventMeta?: WorkflowHistoryEventMeta
}

View File

@ -15,7 +15,7 @@ import { useNodeLoopInteractions } from './hooks'
const Node: FC<NodeProps<LoopNodeType>> = ({
id,
data,
data: _data,
}) => {
const { zoom } = useViewport()
const nodesInitialized = useNodesInitialized()

View File

@ -19,7 +19,7 @@ type MailAndPasswordAuthProps = {
allowRegistration: boolean
}
export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegistration }: MailAndPasswordAuthProps) {
export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegistration: _allowRegistration }: MailAndPasswordAuthProps) {
const { t } = useTranslation()
const { locale } = useContext(I18NContext)
const router = useRouter()

View File

@ -14,7 +14,8 @@ export type Locale = typeof i18n['locales'][number]
export const setLocaleOnClient = async (locale: Locale, reloadPage = true) => {
Cookies.set(LOCALE_COOKIE_NAME, locale, { expires: 365 })
await changeLanguage(locale)
reloadPage && location.reload()
if (reloadPage)
location.reload()
}
export const getLocaleOnClient = (): Locale => {

View File

@ -474,6 +474,7 @@ const translation = {
},
outputVars: {
output: 'Generierter Inhalt',
reasoning_content: 'Reasoning-Inhalt',
usage: 'Nutzungsinformationen des Modells',
},
singleRun: {

View File

@ -490,6 +490,7 @@ const translation = {
},
outputVars: {
output: 'Generate content',
reasoning_content: 'Reasoning Content',
usage: 'Model Usage Information',
},
singleRun: {

View File

@ -474,6 +474,7 @@ const translation = {
},
outputVars: {
output: 'Generar contenido',
reasoning_content: 'Contenido de razonamiento',
usage: 'Información de uso del modelo',
},
singleRun: {

View File

@ -474,6 +474,7 @@ const translation = {
},
outputVars: {
output: 'تولید محتوا',
reasoning_content: 'محتوای استدلال',
usage: 'اطلاعات استفاده از مدل',
},
singleRun: {

View File

@ -474,6 +474,7 @@ const translation = {
},
outputVars: {
output: 'Contenu généré',
reasoning_content: 'Contenu de raisonnement',
usage: 'Informations sur l\'utilisation du modèle',
},
singleRun: {

View File

@ -487,6 +487,7 @@ const translation = {
},
outputVars: {
output: 'सामग्री उत्पन्न करें',
reasoning_content: 'तर्क सामग्री',
usage: 'मॉडल उपयोग जानकारी',
},
singleRun: {

View File

@ -457,6 +457,7 @@ const translation = {
},
outputVars: {
output: 'Hasilkan konten',
reasoning_content: 'Konten penalaran',
usage: 'Informasi Penggunaan Model',
},
singleRun: {

View File

@ -491,6 +491,7 @@ const translation = {
},
outputVars: {
output: 'Genera contenuto',
reasoning_content: 'Contenuto del ragionamento',
usage: 'Informazioni sull\'utilizzo del modello',
},
singleRun: {

View File

@ -486,6 +486,7 @@ const translation = {
},
outputVars: {
output: '生成内容',
reasoning_content: '推論内容',
usage: 'モデル使用量',
},
singleRun: {

View File

@ -499,6 +499,7 @@ const translation = {
},
outputVars: {
output: '생성된 내용',
reasoning_content: '추론 내용',
usage: '모델 사용 정보',
},
singleRun: {

View File

@ -444,6 +444,7 @@ const translation = {
},
outputVars: {
output: 'Generowana treść',
reasoning_content: 'Treść rozumowania',
usage: 'Informacje o użyciu modelu',
},
singleRun: {

View File

@ -444,6 +444,7 @@ const translation = {
},
outputVars: {
output: 'Conteúdo gerado',
reasoning_content: 'Conteúdo de raciocínio',
usage: 'Informações de uso do modelo',
},
singleRun: {

View File

@ -444,6 +444,7 @@ const translation = {
},
outputVars: {
output: 'Conținut generat',
reasoning_content: 'Conținut de raționament',
usage: 'Informații de utilizare a modelului',
},
singleRun: {

View File

@ -444,6 +444,7 @@ const translation = {
},
outputVars: {
output: 'Создать контент',
reasoning_content: 'Содержимое рассуждений',
usage: 'Информация об использовании модели',
},
singleRun: {

Some files were not shown because too many files have changed in this diff Show More