瀏覽代碼

feat: support chatflow start node custom input field hidden (#19678)

tags/1.4.1
Panpan 5 月之前
父節點
當前提交
ef3569e667
No account linked to committer's email address

+ 1
- 0
api/core/app/app_config/entities.py 查看文件

description: str = "" description: str = ""
type: VariableEntityType type: VariableEntityType
required: bool = False required: bool = False
hide: bool = False
max_length: Optional[int] = None max_length: Optional[int] = None
options: Sequence[str] = Field(default_factory=list) options: Sequence[str] = Field(default_factory=list)
allowed_file_types: Sequence[FileType] = Field(default_factory=list) allowed_file_types: Sequence[FileType] = Field(default_factory=list)

+ 6
- 1
web/app/components/app/configuration/config-var/config-modal/index.tsx 查看文件

)} )}


<div className='!mt-5 flex h-6 items-center space-x-2'> <div className='!mt-5 flex h-6 items-center space-x-2'>
<Checkbox checked={tempPayload.required} onCheck={() => handlePayloadChange('required')(!tempPayload.required)} />
<Checkbox checked={tempPayload.required} disabled={tempPayload.hide} onCheck={() => handlePayloadChange('required')(!tempPayload.required)} />
<span className='system-sm-semibold text-text-secondary'>{t('appDebug.variableConfig.required')}</span> <span className='system-sm-semibold text-text-secondary'>{t('appDebug.variableConfig.required')}</span>
</div> </div>

<div className='!mt-5 flex h-6 items-center space-x-2'>
<Checkbox checked={tempPayload.hide} disabled={tempPayload.required} onCheck={() => handlePayloadChange('hide')(!tempPayload.hide)} />
<span className='system-sm-semibold text-text-secondary'>{t('appDebug.variableConfig.hide')}</span>
</div>
</div> </div>
</div> </div>
<ModalFoot <ModalFoot

+ 9
- 5
web/app/components/base/chat/chat-with-history/chat-wrapper.tsx 查看文件

clearChatList, clearChatList,
setClearChatList, setClearChatList,
setIsResponding, setIsResponding,
allInputsHidden,
} = useChatWithHistoryContext() } = useChatWithHistoryContext()
const appConfig = useMemo(() => { const appConfig = useMemo(() => {
const config = appParams || {} const config = appParams || {}
) )
const inputsFormValue = currentConversationId ? currentConversationInputs : newConversationInputsRef?.current const inputsFormValue = currentConversationId ? currentConversationInputs : newConversationInputsRef?.current
const inputDisabled = useMemo(() => { const inputDisabled = useMemo(() => {
if (allInputsHidden)
return false

let hasEmptyInput = '' let hasEmptyInput = ''
let fileIsUploading = false let fileIsUploading = false
const requiredVars = inputsForms.filter(({ required }) => required) const requiredVars = inputsForms.filter(({ required }) => required)
if (fileIsUploading) if (fileIsUploading)
return true return true
return false return false
}, [inputsFormValue, inputsForms])
}, [inputsFormValue, inputsForms, allInputsHidden])


useEffect(() => { useEffect(() => {
if (currentChatInstanceRef.current) if (currentChatInstanceRef.current)
const [collapsed, setCollapsed] = useState(!!currentConversationId) const [collapsed, setCollapsed] = useState(!!currentConversationId)


const chatNode = useMemo(() => { const chatNode = useMemo(() => {
if (!inputsForms.length)
if (allInputsHidden || !inputsForms.length)
return null return null
if (isMobile) { if (isMobile) {
if (!currentConversationId) if (!currentConversationId)
else { else {
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} /> return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
} }
}, [inputsForms.length, isMobile, currentConversationId, collapsed])
}, [inputsForms.length, isMobile, currentConversationId, collapsed, allInputsHidden])


const welcome = useMemo(() => { const welcome = useMemo(() => {
const welcomeMessage = chatList.find(item => item.isOpeningStatement) const welcomeMessage = chatList.find(item => item.isOpeningStatement)
return null return null
if (!welcomeMessage) if (!welcomeMessage)
return null return null
if (!collapsed && inputsForms.length > 0)
if (!collapsed && inputsForms.length > 0 && !allInputsHidden)
return null return null
if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) { if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) {
return ( return (
</div> </div>
</div> </div>
) )
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState])
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState, allInputsHidden])


const answerIcon = (appData?.site && appData.site.use_icon_as_answer_icon) const answerIcon = (appData?.site && appData.site.use_icon_as_answer_icon)
? <AnswerIcon ? <AnswerIcon

+ 2
- 0
web/app/components/base/chat/chat-with-history/context.tsx 查看文件

setIsResponding: (state: boolean) => void, setIsResponding: (state: boolean) => void,
currentConversationInputs: Record<string, any> | null, currentConversationInputs: Record<string, any> | null,
setCurrentConversationInputs: (v: Record<string, any>) => void, setCurrentConversationInputs: (v: Record<string, any>) => void,
allInputsHidden: boolean,
} }


export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
setIsResponding: noop, setIsResponding: noop,
currentConversationInputs: {}, currentConversationInputs: {},
setCurrentConversationInputs: noop, setCurrentConversationInputs: noop,
allInputsHidden: false,
}) })
export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext) export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)

+ 10
- 1
web/app/components/base/chat/chat-with-history/hooks.tsx 查看文件

} }
}) })
}, [appParams]) }, [appParams])

const allInputsHidden = useMemo(() => {
return inputsForms.length > 0 && inputsForms.every(item => item.hide === true)
}, [inputsForms])

useEffect(() => { useEffect(() => {
const conversationInputs: Record<string, any> = {} const conversationInputs: Record<string, any> = {}




const { notify } = useToastContext() const { notify } = useToastContext()
const checkInputsRequired = useCallback((silent?: boolean) => { const checkInputsRequired = useCallback((silent?: boolean) => {
if (allInputsHidden)
return true

let hasEmptyInput = '' let hasEmptyInput = ''
let fileIsUploading = false let fileIsUploading = false
const requiredVars = inputsForms.filter(({ required }) => required) const requiredVars = inputsForms.filter(({ required }) => required)
} }


return true return true
}, [inputsForms, notify, t])
}, [inputsForms, notify, t, allInputsHidden])
const handleStartChat = useCallback((callback: any) => { const handleStartChat = useCallback((callback: any) => {
if (checkInputsRequired()) { if (checkInputsRequired()) {
setShowNewConversationItemInList(true) setShowNewConversationItemInList(true)
setIsResponding, setIsResponding,
currentConversationInputs, currentConversationInputs,
setCurrentConversationInputs, setCurrentConversationInputs,
allInputsHidden,
} }
} }

+ 2
- 0
web/app/components/base/chat/chat-with-history/index.tsx 查看文件

setIsResponding, setIsResponding,
currentConversationInputs, currentConversationInputs,
setCurrentConversationInputs, setCurrentConversationInputs,
allInputsHidden,
} = useChatWithHistory(installedAppInfo) } = useChatWithHistory(installedAppInfo)


return ( return (
setIsResponding, setIsResponding,
currentConversationInputs, currentConversationInputs,
setCurrentConversationInputs, setCurrentConversationInputs,
allInputsHidden,
}}> }}>
<ChatWithHistory className={className} /> <ChatWithHistory className={className} />
</ChatWithHistoryContext.Provider> </ChatWithHistoryContext.Provider>

+ 3
- 1
web/app/components/base/chat/chat-with-history/inputs-form/content.tsx 查看文件

}) })
}, [newConversationInputsRef, handleNewConversationInputsChange, currentConversationInputs, setCurrentConversationInputs]) }, [newConversationInputsRef, handleNewConversationInputsChange, currentConversationInputs, setCurrentConversationInputs])


const visibleInputsForms = inputsForms.filter(form => form.hide !== true)

return ( return (
<div className='space-y-4'> <div className='space-y-4'>
{inputsForms.map(form => (
{visibleInputsForms.map(form => (
<div key={form.variable} className='space-y-1'> <div key={form.variable} className='space-y-1'>
<div className='flex h-6 items-center gap-1'> <div className='flex h-6 items-center gap-1'>
<div className='system-md-semibold text-text-secondary'>{form.label}</div> <div className='system-md-semibold text-text-secondary'>{form.label}</div>

+ 5
- 0
web/app/components/base/chat/chat-with-history/inputs-form/index.tsx 查看文件

isMobile, isMobile,
currentConversationId, currentConversationId,
handleStartChat, handleStartChat,
allInputsHidden,
themeBuilder, themeBuilder,
inputsForms,
} = useChatWithHistoryContext() } = useChatWithHistoryContext()


if (allInputsHidden || inputsForms.length === 0)
return null

return ( return (
<div className={cn('flex flex-col items-center px-4 pt-6', isMobile && 'pt-4')}> <div className={cn('flex flex-col items-center px-4 pt-6', isMobile && 'pt-4')}>
<div className={cn( <div className={cn(

+ 1
- 0
web/app/components/base/chat/chat/type.ts 查看文件

label: string label: string
variable: any variable: any
required: boolean required: boolean
hide: boolean
[key: string]: any [key: string]: any
} }

+ 9
- 5
web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx 查看文件

clearChatList, clearChatList,
setClearChatList, setClearChatList,
setIsResponding, setIsResponding,
allInputsHidden,
} = useEmbeddedChatbotContext() } = useEmbeddedChatbotContext()
const appConfig = useMemo(() => { const appConfig = useMemo(() => {
const config = appParams || {} const config = appParams || {}
) )
const inputsFormValue = currentConversationId ? currentConversationInputs : newConversationInputsRef?.current const inputsFormValue = currentConversationId ? currentConversationInputs : newConversationInputsRef?.current
const inputDisabled = useMemo(() => { const inputDisabled = useMemo(() => {
if (allInputsHidden)
return false

let hasEmptyInput = '' let hasEmptyInput = ''
let fileIsUploading = false let fileIsUploading = false
const requiredVars = inputsForms.filter(({ required }) => required) const requiredVars = inputsForms.filter(({ required }) => required)
if (fileIsUploading) if (fileIsUploading)
return true return true
return false return false
}, [inputsFormValue, inputsForms])
}, [inputsFormValue, inputsForms, allInputsHidden])


useEffect(() => { useEffect(() => {
if (currentChatInstanceRef.current) if (currentChatInstanceRef.current)
const [collapsed, setCollapsed] = useState(!!currentConversationId) const [collapsed, setCollapsed] = useState(!!currentConversationId)


const chatNode = useMemo(() => { const chatNode = useMemo(() => {
if (!inputsForms.length)
if (allInputsHidden || !inputsForms.length)
return null return null
if (isMobile) { if (isMobile) {
if (!currentConversationId) if (!currentConversationId)
else { else {
return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} /> return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
} }
}, [inputsForms.length, isMobile, currentConversationId, collapsed])
}, [inputsForms.length, isMobile, currentConversationId, collapsed, allInputsHidden])


const welcome = useMemo(() => { const welcome = useMemo(() => {
const welcomeMessage = chatList.find(item => item.isOpeningStatement) const welcomeMessage = chatList.find(item => item.isOpeningStatement)
return null return null
if (!welcomeMessage) if (!welcomeMessage)
return null return null
if (!collapsed && inputsForms.length > 0)
if (!collapsed && inputsForms.length > 0 && !allInputsHidden)
return null return null
if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) { if (welcomeMessage.suggestedQuestions && welcomeMessage.suggestedQuestions?.length > 0) {
return ( return (
</div> </div>
</div> </div>
) )
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState])
}, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length, respondingState, allInputsHidden])


const answerIcon = isDify() const answerIcon = isDify()
? <LogoAvatar className='relative shrink-0' /> ? <LogoAvatar className='relative shrink-0' />

+ 2
- 0
web/app/components/base/chat/embedded-chatbot/context.tsx 查看文件

setIsResponding: (state: boolean) => void, setIsResponding: (state: boolean) => void,
currentConversationInputs: Record<string, any> | null, currentConversationInputs: Record<string, any> | null,
setCurrentConversationInputs: (v: Record<string, any>) => void, setCurrentConversationInputs: (v: Record<string, any>) => void,
allInputsHidden: boolean
} }


export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
setIsResponding: noop, setIsResponding: noop,
currentConversationInputs: {}, currentConversationInputs: {},
setCurrentConversationInputs: noop, setCurrentConversationInputs: noop,
allInputsHidden: false,
}) })
export const useEmbeddedChatbotContext = () => useContext(EmbeddedChatbotContext) export const useEmbeddedChatbotContext = () => useContext(EmbeddedChatbotContext)

+ 9
- 1
web/app/components/base/chat/embedded-chatbot/hooks.tsx 查看文件

}) })
}, [initInputs, appParams]) }, [initInputs, appParams])


const allInputsHidden = useMemo(() => {
return inputsForms.length > 0 && inputsForms.every(item => item.hide === true)
}, [inputsForms])

useEffect(() => { useEffect(() => {
// init inputs from url params // init inputs from url params
(async () => { (async () => {


const { notify } = useToastContext() const { notify } = useToastContext()
const checkInputsRequired = useCallback((silent?: boolean) => { const checkInputsRequired = useCallback((silent?: boolean) => {
if (allInputsHidden)
return true

let hasEmptyInput = '' let hasEmptyInput = ''
let fileIsUploading = false let fileIsUploading = false
const requiredVars = inputsForms.filter(({ required }) => required) const requiredVars = inputsForms.filter(({ required }) => required)
} }


return true return true
}, [inputsForms, notify, t])
}, [inputsForms, notify, t, allInputsHidden])
const handleStartChat = useCallback((callback?: any) => { const handleStartChat = useCallback((callback?: any) => {
if (checkInputsRequired()) { if (checkInputsRequired()) {
setShowNewConversationItemInList(true) setShowNewConversationItemInList(true)
setIsResponding, setIsResponding,
currentConversationInputs, currentConversationInputs,
setCurrentConversationInputs, setCurrentConversationInputs,
allInputsHidden,
} }
} }

+ 2
- 0
web/app/components/base/chat/embedded-chatbot/index.tsx 查看文件

setIsResponding, setIsResponding,
currentConversationInputs, currentConversationInputs,
setCurrentConversationInputs, setCurrentConversationInputs,
allInputsHidden,
} = useEmbeddedChatbot() } = useEmbeddedChatbot()


return <EmbeddedChatbotContext.Provider value={{ return <EmbeddedChatbotContext.Provider value={{
setIsResponding, setIsResponding,
currentConversationInputs, currentConversationInputs,
setCurrentConversationInputs, setCurrentConversationInputs,
allInputsHidden,
}}> }}>
<Chatbot /> <Chatbot />
</EmbeddedChatbotContext.Provider> </EmbeddedChatbotContext.Provider>

+ 3
- 1
web/app/components/base/chat/embedded-chatbot/inputs-form/content.tsx 查看文件

}) })
}, [newConversationInputsRef, handleNewConversationInputsChange, currentConversationInputs, setCurrentConversationInputs]) }, [newConversationInputsRef, handleNewConversationInputsChange, currentConversationInputs, setCurrentConversationInputs])


const visibleInputsForms = inputsForms.filter(form => form.hide !== true)

return ( return (
<div className='space-y-4'> <div className='space-y-4'>
{inputsForms.map(form => (
{visibleInputsForms.map(form => (
<div key={form.variable} className='space-y-1'> <div key={form.variable} className='space-y-1'>
<div className='flex h-6 items-center gap-1'> <div className='flex h-6 items-center gap-1'>
<div className='system-md-semibold text-text-secondary'>{form.label}</div> <div className='system-md-semibold text-text-secondary'>{form.label}</div>

+ 5
- 0
web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx 查看文件

currentConversationId, currentConversationId,
themeBuilder, themeBuilder,
handleStartChat, handleStartChat,
allInputsHidden,
inputsForms,
} = useEmbeddedChatbotContext() } = useEmbeddedChatbotContext()


if (allInputsHidden || inputsForms.length === 0)
return null

return ( return (
<div className={cn('mb-6 flex flex-col items-center px-4 pt-6', isMobile && 'mb-4 pt-4')}> <div className={cn('mb-6 flex flex-col items-center px-4 pt-6', isMobile && 'mb-4 pt-4')}>
<div className={cn( <div className={cn(

+ 2
- 1
web/app/components/workflow/panel/debug-and-preview/index.tsx 查看文件

const nodes = useNodes<StartNodeType>() const nodes = useNodes<StartNodeType>()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start) const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const variables = startNode?.data.variables || [] const variables = startNode?.data.variables || []
const visibleVariables = variables.filter(v => v.hide !== true)


const [showConversationVariableModal, setShowConversationVariableModal] = useState(false) const [showConversationVariableModal, setShowConversationVariableModal] = useState(false)


</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
)} )}
{variables.length > 0 && (
{visibleVariables.length > 0 && (
<div className='relative'> <div className='relative'>
<Tooltip <Tooltip
popupContent={t('workflow.panel.userInputField')} popupContent={t('workflow.panel.userInputField')}

+ 3
- 2
web/app/components/workflow/panel/debug-and-preview/user-input.tsx 查看文件

const nodes = useNodes<StartNodeType>() const nodes = useNodes<StartNodeType>()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start) const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const variables = startNode?.data.variables || [] const variables = startNode?.data.variables || []
const visibleVariables = variables.filter(v => v.hide !== true)


const handleValueChange = (variable: string, v: string) => { const handleValueChange = (variable: string, v: string) => {
const { const {
}) })
} }


if (!variables.length)
if (!visibleVariables.length)
return null return null


return ( return (
<div className={cn('sticky top-0 z-[1] rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg shadow-xs')}> <div className={cn('sticky top-0 z-[1] rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg shadow-xs')}>
<div className='px-4 pb-4 pt-3'> <div className='px-4 pb-4 pt-3'>
{variables.map((variable, index) => (
{visibleVariables.map((variable, index) => (
<div <div
key={variable.variable} key={variable.variable}
className='mb-4 last-of-type:mb-0' className='mb-4 last-of-type:mb-0'

+ 1
- 0
web/app/components/workflow/types.ts 查看文件

hint?: string hint?: string
options?: string[] options?: string[]
value_selector?: ValueSelector value_selector?: ValueSelector
hide: boolean
} & Partial<UploadFileSetting> } & Partial<UploadFileSetting>


export type ModelConfig = { export type ModelConfig = {

+ 1
- 0
web/i18n/en-US/app-debug.ts 查看文件

'inputPlaceholder': 'Please input', 'inputPlaceholder': 'Please input',
'content': 'Content', 'content': 'Content',
'required': 'Required', 'required': 'Required',
'hide': 'Hide',
'file': { 'file': {
supportFileTypes: 'Support File Types', supportFileTypes: 'Support File Types',
image: { image: {

Loading…
取消
儲存