mirror of https://github.com/langgenius/dify.git
Feat/web workflow improvements (#27981)
Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> Co-authored-by: johnny0120 <johnny0120@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Wood <tuiskuwood@outlook.com>
This commit is contained in:
parent
ce00388278
commit
abc13ef762
|
|
@ -155,8 +155,17 @@ class BaseAppGenerator:
|
|||
f"{variable_entity.variable} in input form must be less than {variable_entity.max_length} files"
|
||||
)
|
||||
case VariableEntityType.CHECKBOX:
|
||||
if not isinstance(value, bool):
|
||||
raise ValueError(f"{variable_entity.variable} in input form must be a valid boolean value")
|
||||
if isinstance(value, str):
|
||||
normalized_value = value.strip().lower()
|
||||
if normalized_value in {"true", "1", "yes", "on"}:
|
||||
value = True
|
||||
elif normalized_value in {"false", "0", "no", "off"}:
|
||||
value = False
|
||||
elif isinstance(value, (int, float)):
|
||||
if value == 1:
|
||||
value = True
|
||||
elif value == 0:
|
||||
value = False
|
||||
case _:
|
||||
raise AssertionError("this statement should be unreachable.")
|
||||
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ class WorkflowToolProviderController(ToolProviderController):
|
|||
form=parameter.form,
|
||||
llm_description=parameter.description,
|
||||
required=variable.required,
|
||||
default=variable.default,
|
||||
options=options,
|
||||
placeholder=I18nObject(en_US="", zh_Hans=""),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -229,6 +229,8 @@ def _get_file_extract_string_func(*, key: str) -> Callable[[File], str]:
|
|||
return lambda x: x.transfer_method
|
||||
case "url":
|
||||
return lambda x: x.remote_url or ""
|
||||
case "related_id":
|
||||
return lambda x: x.related_id or ""
|
||||
case _:
|
||||
raise InvalidKeyError(f"Invalid key: {key}")
|
||||
|
||||
|
|
@ -299,7 +301,7 @@ def _get_boolean_filter_func(*, condition: FilterOperator, value: bool) -> Calla
|
|||
|
||||
def _get_file_filter_func(*, key: str, condition: str, value: str | Sequence[str]) -> Callable[[File], bool]:
|
||||
extract_func: Callable[[File], Any]
|
||||
if key in {"name", "extension", "mime_type", "url"} and isinstance(value, str):
|
||||
if key in {"name", "extension", "mime_type", "url", "related_id"} and isinstance(value, str):
|
||||
extract_func = _get_file_extract_string_func(key=key)
|
||||
return lambda x: _get_string_filter_func(condition=condition, value=value)(extract_func(x))
|
||||
if key in {"type", "transfer_method"}:
|
||||
|
|
@ -358,7 +360,7 @@ def _ge(value: int | float) -> Callable[[int | float], bool]:
|
|||
|
||||
def _order_file(*, order: Order, order_by: str = "", array: Sequence[File]):
|
||||
extract_func: Callable[[File], Any]
|
||||
if order_by in {"name", "type", "extension", "mime_type", "transfer_method", "url"}:
|
||||
if order_by in {"name", "type", "extension", "mime_type", "transfer_method", "url", "related_id"}:
|
||||
extract_func = _get_file_extract_string_func(key=order_by)
|
||||
return sorted(array, key=lambda x: extract_func(x), reverse=order == Order.DESC)
|
||||
elif order_by == "size":
|
||||
|
|
|
|||
|
|
@ -550,7 +550,7 @@ class AppDslService:
|
|||
"app": {
|
||||
"name": app_model.name,
|
||||
"mode": app_model.mode,
|
||||
"icon": "🤖" if app_model.icon_type == "image" else app_model.icon,
|
||||
"icon": app_model.icon if app_model.icon_type == "image" else "🤖",
|
||||
"icon_background": "#FFEAD5" if app_model.icon_type == "image" else app_model.icon_background,
|
||||
"description": app_model.description,
|
||||
"use_icon_as_answer_icon": app_model.use_icon_as_answer_icon,
|
||||
|
|
|
|||
|
|
@ -1375,6 +1375,11 @@ class DocumentService:
|
|||
|
||||
document.name = name
|
||||
db.session.add(document)
|
||||
if document.data_source_info_dict:
|
||||
db.session.query(UploadFile).where(
|
||||
UploadFile.id == document.data_source_info_dict["upload_file_id"]
|
||||
).update({UploadFile.name: name})
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return document
|
||||
|
|
|
|||
|
|
@ -109,6 +109,13 @@ const ConfigModal: FC<IConfigModalProps> = ({
|
|||
[key]: value,
|
||||
}
|
||||
|
||||
// Clear default value if modified options no longer include current default
|
||||
if (key === 'options' && prev.default) {
|
||||
const optionsArray = Array.isArray(value) ? value : []
|
||||
if (!optionsArray.includes(prev.default))
|
||||
newPayload.default = undefined
|
||||
}
|
||||
|
||||
return newPayload
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ const ConfigSelect: FC<IConfigSelectProps> = ({
|
|||
className='absolute right-1.5 top-1/2 block translate-y-[-50%] cursor-pointer rounded-md p-1 text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive'
|
||||
onClick={() => {
|
||||
onChange(options.filter((_, i) => index !== i))
|
||||
setDeletingID(null)
|
||||
}}
|
||||
onMouseEnter={() => setDeletingID(index)}
|
||||
onMouseLeave={() => setDeletingID(null)}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import ConfigContext from '@/context/debug-configuration'
|
||||
|
|
@ -32,6 +32,24 @@ const ChatUserInput = ({
|
|||
return obj
|
||||
})()
|
||||
|
||||
// Initialize inputs with default values from promptVariables
|
||||
useEffect(() => {
|
||||
const newInputs = { ...inputs }
|
||||
let hasChanges = false
|
||||
|
||||
promptVariables.forEach((variable) => {
|
||||
const { key, default: defaultValue } = variable
|
||||
// Only set default value if the field is empty and a default exists
|
||||
if (defaultValue !== undefined && defaultValue !== null && defaultValue !== '' && (inputs[key] === undefined || inputs[key] === null || inputs[key] === '')) {
|
||||
newInputs[key] = defaultValue
|
||||
hasChanges = true
|
||||
}
|
||||
})
|
||||
|
||||
if (hasChanges)
|
||||
setInputs(newInputs)
|
||||
}, [promptVariables, inputs, setInputs])
|
||||
|
||||
const handleInputValueChange = (key: string, value: string | boolean) => {
|
||||
if (!(key in promptVariableObj))
|
||||
return
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import {
|
||||
|
|
@ -54,6 +54,24 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
|||
return obj
|
||||
}, [promptVariables])
|
||||
|
||||
// Initialize inputs with default values from promptVariables
|
||||
useEffect(() => {
|
||||
const newInputs = { ...inputs }
|
||||
let hasChanges = false
|
||||
|
||||
promptVariables.forEach((variable) => {
|
||||
const { key, default: defaultValue } = variable
|
||||
// Only set default value if the field is empty and a default exists
|
||||
if (defaultValue !== undefined && defaultValue !== null && defaultValue !== '' && (inputs[key] === undefined || inputs[key] === null || inputs[key] === '')) {
|
||||
newInputs[key] = defaultValue
|
||||
hasChanges = true
|
||||
}
|
||||
})
|
||||
|
||||
if (hasChanges)
|
||||
setInputs(newInputs)
|
||||
}, [promptVariables, inputs, setInputs])
|
||||
|
||||
const canNotRun = useMemo(() => {
|
||||
if (mode !== AppModeEnum.COMPLETION)
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ import { Theme } from '@/types/app'
|
|||
import cn from '@/utils/classnames'
|
||||
|
||||
type AudioPlayerProps = {
|
||||
src: string
|
||||
src?: string // Keep backward compatibility
|
||||
srcs?: string[] // Support multiple sources
|
||||
}
|
||||
|
||||
const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
|
||||
const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
const [duration, setDuration] = useState(0)
|
||||
|
|
@ -61,19 +62,22 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
|
|||
// Preload audio metadata
|
||||
audio.load()
|
||||
|
||||
// Delayed generation of waveform data
|
||||
// eslint-disable-next-line ts/no-use-before-define
|
||||
const timer = setTimeout(() => generateWaveformData(src), 1000)
|
||||
|
||||
return () => {
|
||||
audio.removeEventListener('loadedmetadata', setAudioData)
|
||||
audio.removeEventListener('timeupdate', setAudioTime)
|
||||
audio.removeEventListener('progress', handleProgress)
|
||||
audio.removeEventListener('ended', handleEnded)
|
||||
audio.removeEventListener('error', handleError)
|
||||
clearTimeout(timer)
|
||||
// Use the first source or src to generate waveform
|
||||
const primarySrc = srcs?.[0] || src
|
||||
if (primarySrc) {
|
||||
// Delayed generation of waveform data
|
||||
// eslint-disable-next-line ts/no-use-before-define
|
||||
const timer = setTimeout(() => generateWaveformData(primarySrc), 1000)
|
||||
return () => {
|
||||
audio.removeEventListener('loadedmetadata', setAudioData)
|
||||
audio.removeEventListener('timeupdate', setAudioTime)
|
||||
audio.removeEventListener('progress', handleProgress)
|
||||
audio.removeEventListener('ended', handleEnded)
|
||||
audio.removeEventListener('error', handleError)
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
}, [src])
|
||||
}, [src, srcs])
|
||||
|
||||
const generateWaveformData = async (audioSrc: string) => {
|
||||
if (!window.AudioContext && !(window as any).webkitAudioContext) {
|
||||
|
|
@ -85,8 +89,9 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
|
|||
return null
|
||||
}
|
||||
|
||||
const url = new URL(src)
|
||||
const isHttp = url.protocol === 'http:' || url.protocol === 'https:'
|
||||
const primarySrc = srcs?.[0] || src
|
||||
const url = primarySrc ? new URL(primarySrc) : null
|
||||
const isHttp = url ? (url.protocol === 'http:' || url.protocol === 'https:') : false
|
||||
if (!isHttp) {
|
||||
setIsAudioAvailable(false)
|
||||
return null
|
||||
|
|
@ -286,8 +291,13 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
|
|||
}, [duration])
|
||||
|
||||
return (
|
||||
<div className='flex h-9 min-w-[240px] max-w-[420px] items-end gap-2 rounded-[10px] border border-components-panel-border-subtle bg-components-chat-input-audio-bg-alt p-2 shadow-xs backdrop-blur-sm'>
|
||||
<audio ref={audioRef} src={src} preload="auto"/>
|
||||
<div className='flex h-9 min-w-[240px] max-w-[420px] items-center gap-2 rounded-[10px] border border-components-panel-border-subtle bg-components-chat-input-audio-bg-alt p-2 shadow-xs backdrop-blur-sm'>
|
||||
<audio ref={audioRef} src={src} preload="auto">
|
||||
{/* If srcs array is provided, render multiple source elements */}
|
||||
{srcs && srcs.map((srcUrl, index) => (
|
||||
<source key={index} src={srcUrl} />
|
||||
))}
|
||||
</audio>
|
||||
<button type="button" className='inline-flex shrink-0 cursor-pointer items-center justify-center border-none text-text-accent transition-all hover:text-text-accent-secondary disabled:text-components-button-primary-bg-disabled' onClick={togglePlay} disabled={!isAudioAvailable}>
|
||||
{isPlaying
|
||||
? (
|
||||
|
|
|
|||
|
|
@ -6,7 +6,14 @@ type Props = {
|
|||
}
|
||||
|
||||
const AudioGallery: React.FC<Props> = ({ srcs }) => {
|
||||
return (<><br/>{srcs.map((src, index) => (<AudioPlayer key={`audio_${index}`} src={src}/>))}</>)
|
||||
const validSrcs = srcs.filter(src => src)
|
||||
if (validSrcs.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="my-3">
|
||||
<AudioPlayer srcs={validSrcs} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(AudioGallery)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
import Textarea from 'react-textarea-autosize'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Recorder from 'js-audio-recorder'
|
||||
import { decode } from 'html-entities'
|
||||
import type {
|
||||
EnableType,
|
||||
OnSend,
|
||||
|
|
@ -203,7 +204,7 @@ const ChatInputArea = ({
|
|||
className={cn(
|
||||
'body-lg-regular w-full resize-none bg-transparent p-1 leading-6 text-text-primary outline-none',
|
||||
)}
|
||||
placeholder={t('common.chat.inputPlaceholder', { botName }) || ''}
|
||||
placeholder={decode(t('common.chat.inputPlaceholder', { botName }) || '')}
|
||||
autoFocus
|
||||
minRows={1}
|
||||
value={query}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import type { InputVar } from '@/app/components/workflow/types'
|
|||
import { getNewVar } from '@/utils/var'
|
||||
import cn from '@/utils/classnames'
|
||||
import { noop } from 'lodash-es'
|
||||
import { checkKeys } from '@/utils/var'
|
||||
|
||||
type OpeningSettingModalProps = {
|
||||
data: OpeningStatement
|
||||
|
|
@ -53,7 +54,10 @@ const OpeningSettingModal = ({
|
|||
return
|
||||
|
||||
if (!ignoreVariablesCheck) {
|
||||
const keys = getInputKeys(tempValue)
|
||||
const keys = getInputKeys(tempValue)?.filter((key) => {
|
||||
const { isValid } = checkKeys([key], true)
|
||||
return isValid
|
||||
})
|
||||
const promptKeys = promptVariables.map(item => item.key)
|
||||
const workflowVariableKeys = workflowVariables.map(item => item.variable)
|
||||
let notIncludeKeys: string[] = []
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|||
import styles from './VideoPlayer.module.css'
|
||||
|
||||
type VideoPlayerProps = {
|
||||
src: string
|
||||
src?: string // Keep backward compatibility
|
||||
srcs?: string[] // Support multiple sources
|
||||
}
|
||||
|
||||
const PlayIcon = () => (
|
||||
|
|
@ -35,7 +36,7 @@ const FullscreenIcon = () => (
|
|||
</svg>
|
||||
)
|
||||
|
||||
const VideoPlayer: React.FC<VideoPlayerProps> = ({ src }) => {
|
||||
const VideoPlayer: React.FC<VideoPlayerProps> = ({ src, srcs }) => {
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
const [duration, setDuration] = useState(0)
|
||||
|
|
@ -78,7 +79,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ src }) => {
|
|||
video.removeEventListener('timeupdate', setVideoTime)
|
||||
video.removeEventListener('ended', handleEnded)
|
||||
}
|
||||
}, [src])
|
||||
}, [src, srcs])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
|
@ -131,7 +132,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ src }) => {
|
|||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const updateVideoProgress = useCallback((clientX: number) => {
|
||||
const updateVideoProgress = useCallback((clientX: number, updateTime = false) => {
|
||||
const progressBar = progressRef.current
|
||||
const video = videoRef.current
|
||||
if (progressBar && video) {
|
||||
|
|
@ -140,7 +141,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ src }) => {
|
|||
const newTime = pos * video.duration
|
||||
if (newTime >= 0 && newTime <= video.duration) {
|
||||
setHoverTime(newTime)
|
||||
if (isDragging)
|
||||
if (isDragging || updateTime)
|
||||
video.currentTime = newTime
|
||||
}
|
||||
}
|
||||
|
|
@ -155,10 +156,15 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ src }) => {
|
|||
setHoverTime(null)
|
||||
}, [isDragging])
|
||||
|
||||
const handleProgressClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault()
|
||||
updateVideoProgress(e.clientX, true)
|
||||
}, [updateVideoProgress])
|
||||
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault()
|
||||
setIsDragging(true)
|
||||
updateVideoProgress(e.clientX)
|
||||
updateVideoProgress(e.clientX, true)
|
||||
}, [updateVideoProgress])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -209,14 +215,19 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({ src }) => {
|
|||
|
||||
return (
|
||||
<div ref={containerRef} className={styles.videoPlayer} onMouseMove={showControls} onMouseEnter={showControls}>
|
||||
<video ref={videoRef} src={src} className={styles.video} />
|
||||
<video ref={videoRef} src={src} className={styles.video}>
|
||||
{/* If srcs array is provided, render multiple source elements */}
|
||||
{srcs && srcs.map((srcUrl, index) => (
|
||||
<source key={index} src={srcUrl} />
|
||||
))}
|
||||
</video>
|
||||
<div className={`${styles.controls} ${isControlsVisible ? styles.visible : styles.hidden} ${isSmallSize ? styles.smallSize : ''}`}>
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.progressBarContainer}>
|
||||
<div
|
||||
ref={progressRef}
|
||||
className={styles.progressBar}
|
||||
onClick={handleMouseDown}
|
||||
onClick={handleProgressClick}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseDown={handleMouseDown}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,14 @@ type Props = {
|
|||
}
|
||||
|
||||
const VideoGallery: React.FC<Props> = ({ srcs }) => {
|
||||
return (<><br/>{srcs.map((src, index) => (<React.Fragment key={`video_${index}`}><br/><VideoPlayer src={src}/></React.Fragment>))}</>)
|
||||
const validSrcs = srcs.filter(src => src)
|
||||
if (validSrcs.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="my-3">
|
||||
<VideoPlayer srcs={validSrcs} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(VideoGallery)
|
||||
|
|
|
|||
|
|
@ -384,7 +384,7 @@ const GotoAnything: FC<Props> = ({
|
|||
{results.map(result => (
|
||||
<Command.Item
|
||||
key={`${result.type}-${result.id}`}
|
||||
value={result.title}
|
||||
value={`${result.type}-${result.id}`}
|
||||
className='flex cursor-pointer items-center gap-3 rounded-md p-3 will-change-[background-color] aria-[selected=true]:bg-state-base-hover data-[selected=true]:bg-state-base-hover'
|
||||
onSelect={() => handleNavigate(result)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ const RunOnce: FC<IRunOnceProps> = ({
|
|||
{/* input form */}
|
||||
<form onSubmit={onSubmit}>
|
||||
{(inputs === null || inputs === undefined || Object.keys(inputs).length === 0) || !isInitialized ? null
|
||||
: promptConfig.prompt_variables.map(item => (
|
||||
: promptConfig.prompt_variables.filter(item => item.hide !== true).map(item => (
|
||||
<div className='mt-4 w-full' key={item.key}>
|
||||
{item.type !== 'checkbox' && (
|
||||
<div className='system-md-semibold flex h-6 items-center gap-1 text-text-secondary'>
|
||||
|
|
|
|||
|
|
@ -283,24 +283,15 @@ const BaseNode: FC<BaseNodeProps> = ({
|
|||
data.type === BlockEnum.Loop && data._loopIndex && LoopIndex
|
||||
}
|
||||
{
|
||||
isLoading && (
|
||||
<RiLoader2Line className='h-3.5 w-3.5 animate-spin text-text-accent' />
|
||||
)
|
||||
}
|
||||
{
|
||||
(!isLoading && (data._runningStatus === NodeRunningStatus.Succeeded || hasVarValue)) && (
|
||||
<RiCheckboxCircleFill className='h-3.5 w-3.5 text-text-success' />
|
||||
)
|
||||
}
|
||||
{
|
||||
data._runningStatus === NodeRunningStatus.Failed && (
|
||||
<RiErrorWarningFill className='h-3.5 w-3.5 text-text-destructive' />
|
||||
)
|
||||
}
|
||||
{
|
||||
data._runningStatus === NodeRunningStatus.Exception && (
|
||||
<RiAlertFill className='h-3.5 w-3.5 text-text-warning-secondary' />
|
||||
)
|
||||
isLoading
|
||||
? <RiLoader2Line className='h-3.5 w-3.5 animate-spin text-text-accent' />
|
||||
: data._runningStatus === NodeRunningStatus.Failed
|
||||
? <RiErrorWarningFill className='h-3.5 w-3.5 text-text-destructive' />
|
||||
: data._runningStatus === NodeRunningStatus.Exception
|
||||
? <RiAlertFill className='h-3.5 w-3.5 text-text-warning-secondary' />
|
||||
: (data._runningStatus === NodeRunningStatus.Succeeded || hasVarValue)
|
||||
? <RiCheckboxCircleFill className='h-3.5 w-3.5 text-text-success' />
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -81,6 +81,17 @@ export const getOperators = (type?: VarType, file?: { key: string }) => {
|
|||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
case 'related_id':
|
||||
return [
|
||||
ComparisonOperator.is,
|
||||
ComparisonOperator.isNot,
|
||||
ComparisonOperator.contains,
|
||||
ComparisonOperator.notContains,
|
||||
ComparisonOperator.startWith,
|
||||
ComparisonOperator.endWith,
|
||||
ComparisonOperator.empty,
|
||||
ComparisonOperator.notEmpty,
|
||||
]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type { ValueSelector, Var } from '@/app/components/workflow/types'
|
|||
import { ReactSortable } from 'react-sortablejs'
|
||||
import { noop } from 'lodash-es'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiDraggable } from '@remixicon/react'
|
||||
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.questionClassifiers'
|
||||
|
|
@ -135,9 +136,13 @@ const ClassList: FC<Props> = ({
|
|||
}}
|
||||
>
|
||||
<div>
|
||||
{canDrag && <RiDraggable className={cn(
|
||||
'handle absolute left-2 top-3 hidden h-3 w-3 cursor-pointer text-text-tertiary',
|
||||
'group-hover:block',
|
||||
)} />}
|
||||
<Item
|
||||
className={cn(canDrag && 'handle')}
|
||||
headerClassName={cn(canDrag && 'cursor-grab')}
|
||||
headerClassName={cn(canDrag && 'cursor-grab group-hover:pl-5')}
|
||||
nodeId={nodeId}
|
||||
key={list[index].id}
|
||||
payload={item}
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ const Right = ({
|
|||
</ActionButton>
|
||||
)}
|
||||
<div className='flex w-0 grow items-center gap-1'>
|
||||
{currentNodeVar && (
|
||||
{currentNodeVar?.var && (
|
||||
<>
|
||||
{
|
||||
[VarInInspectType.environment, VarInInspectType.conversation, VarInInspectType.system].includes(currentNodeVar.nodeType as VarInInspectType) && (
|
||||
|
|
@ -264,14 +264,15 @@ const Right = ({
|
|||
</div>
|
||||
{/* content */}
|
||||
<div className='grow p-2'>
|
||||
{!currentNodeVar && <Empty />}
|
||||
{!currentNodeVar?.var && <Empty />}
|
||||
{isValueFetching && (
|
||||
<div className='flex h-full items-center justify-center'>
|
||||
<Loading />
|
||||
</div>
|
||||
)}
|
||||
{currentNodeVar && !isValueFetching && (
|
||||
{currentNodeVar?.var && !isValueFetching && (
|
||||
<ValueContent
|
||||
key={`${currentNodeVar.nodeId}-${currentNodeVar.var.id}`}
|
||||
currentVar={currentNodeVar.var}
|
||||
handleValueChange={handleValueChange}
|
||||
isTruncated={!!isTruncated}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,11 @@ const VariableInspectTrigger: FC = () => {
|
|||
className={cn('system-xs-medium flex h-6 cursor-pointer items-center rounded-md border-[0.5px] border-effects-highlight bg-components-actionbar-bg px-1 text-text-tertiary shadow-lg backdrop-blur-sm hover:bg-components-actionbar-bg-accent hover:text-text-accent',
|
||||
nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled',
|
||||
)}
|
||||
onClick={handleClearAll}
|
||||
onClick={() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
handleClearAll()
|
||||
}}
|
||||
>
|
||||
{t('workflow.debug.variableInspect.trigger.clear')}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@
|
|||
"elkjs": "^0.9.3",
|
||||
"emoji-mart": "^5.6.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"html-entities": "^2.6.0",
|
||||
"html-to-image": "1.11.13",
|
||||
"i18next": "^23.16.8",
|
||||
"i18next-resources-to-backend": "^1.2.1",
|
||||
|
|
|
|||
|
|
@ -174,6 +174,9 @@ importers:
|
|||
fast-deep-equal:
|
||||
specifier: ^3.1.3
|
||||
version: 3.1.3
|
||||
html-entities:
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0
|
||||
html-to-image:
|
||||
specifier: 1.11.13
|
||||
version: 1.11.13
|
||||
|
|
|
|||
Loading…
Reference in New Issue