diff --git a/api/core/app/apps/base_app_generator.py b/api/core/app/apps/base_app_generator.py index 85be05fb69..1c6ca87925 100644 --- a/api/core/app/apps/base_app_generator.py +++ b/api/core/app/apps/base_app_generator.py @@ -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.") diff --git a/api/core/tools/workflow_as_tool/provider.py b/api/core/tools/workflow_as_tool/provider.py index c8e91413cd..cee41ba90f 100644 --- a/api/core/tools/workflow_as_tool/provider.py +++ b/api/core/tools/workflow_as_tool/provider.py @@ -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=""), ) diff --git a/api/core/workflow/nodes/list_operator/node.py b/api/core/workflow/nodes/list_operator/node.py index 180eb2ad90..54f3ef8a54 100644 --- a/api/core/workflow/nodes/list_operator/node.py +++ b/api/core/workflow/nodes/list_operator/node.py @@ -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": diff --git a/api/services/app_dsl_service.py b/api/services/app_dsl_service.py index 15fefd6116..1dd6faea5d 100644 --- a/api/services/app_dsl_service.py +++ b/api/services/app_dsl_service.py @@ -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, diff --git a/api/services/dataset_service.py b/api/services/dataset_service.py index abfb4baeec..2bec61963c 100644 --- a/api/services/dataset_service.py +++ b/api/services/dataset_service.py @@ -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 diff --git a/web/app/components/app/configuration/config-var/config-modal/index.tsx b/web/app/components/app/configuration/config-var/config-modal/index.tsx index bab77e61b0..17df1558d8 100644 --- a/web/app/components/app/configuration/config-var/config-modal/index.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/index.tsx @@ -109,6 +109,13 @@ const ConfigModal: FC = ({ [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 }) } diff --git a/web/app/components/app/configuration/config-var/config-select/index.tsx b/web/app/components/app/configuration/config-var/config-select/index.tsx index 40ddaef78f..713a715f1c 100644 --- a/web/app/components/app/configuration/config-var/config-select/index.tsx +++ b/web/app/components/app/configuration/config-var/config-select/index.tsx @@ -71,6 +71,7 @@ const ConfigSelect: FC = ({ 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)} diff --git a/web/app/components/app/configuration/debug/chat-user-input.tsx b/web/app/components/app/configuration/debug/chat-user-input.tsx index b1161de075..16666d514e 100644 --- a/web/app/components/app/configuration/debug/chat-user-input.tsx +++ b/web/app/components/app/configuration/debug/chat-user-input.tsx @@ -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 diff --git a/web/app/components/app/configuration/prompt-value-panel/index.tsx b/web/app/components/app/configuration/prompt-value-panel/index.tsx index e8b988767c..005f7f938f 100644 --- a/web/app/components/app/configuration/prompt-value-panel/index.tsx +++ b/web/app/components/app/configuration/prompt-value-panel/index.tsx @@ -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 = ({ 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 diff --git a/web/app/components/base/audio-gallery/AudioPlayer.tsx b/web/app/components/base/audio-gallery/AudioPlayer.tsx index cad7adac02..399f055161 100644 --- a/web/app/components/base/audio-gallery/AudioPlayer.tsx +++ b/web/app/components/base/audio-gallery/AudioPlayer.tsx @@ -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 = ({ src }) => { +const AudioPlayer: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ src }) => { }, [duration]) return ( -
-