| 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) |
| )} | )} | ||||
| <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 |
| 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 |
| 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) |
| } | } | ||||
| }) | }) | ||||
| }, [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, | |||||
| } | } | ||||
| } | } |
| 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> |
| }) | }) | ||||
| }, [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> |
| 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( |
| label: string | label: string | ||||
| variable: any | variable: any | ||||
| required: boolean | required: boolean | ||||
| hide: boolean | |||||
| [key: string]: any | [key: string]: any | ||||
| } | } |
| 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' /> |
| 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) |
| }) | }) | ||||
| }, [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, | |||||
| } | } | ||||
| } | } |
| 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> |
| }) | }) | ||||
| }, [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> |
| 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( |
| 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')} |
| 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' |
| 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 = { |
| '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: { |