mirror of https://github.com/langgenius/dify.git
Merge branch 'feat/hitl-frontend' of https://github.com/langgenius/dify into feat/hitl-frontend
This commit is contained in:
commit
18e57096d2
|
|
@ -0,0 +1,25 @@
|
|||
name: Deploy HITL frontend
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build and Push API & Web"]
|
||||
branches:
|
||||
- "feat/hitl-frontend"
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.head_branch == 'feat/hitl-frontend'
|
||||
steps:
|
||||
- name: Deploy to server
|
||||
uses: appleboy/ssh-action@v0.1.8
|
||||
with:
|
||||
host: ${{ secrets.HITL_SSH_HOST }}
|
||||
username: ${{ secrets.SSH_USER }}
|
||||
key: ${{ secrets.SSH_PRIVATE_KEY }}
|
||||
script: |
|
||||
${{ vars.SSH_SCRIPT || secrets.SSH_SCRIPT }}
|
||||
|
|
@ -32,10 +32,12 @@ export type BeforeRunFormProps = {
|
|||
showSpecialResultPanel?: boolean
|
||||
existVarValuesInForms: Record<string, any>[]
|
||||
filteredExistVarForms: FormProps[]
|
||||
generatedFormContentData?: Record<string, any>
|
||||
showGeneratedForm?: boolean
|
||||
handleShowGeneratedForm?: (data: Record<string, any>) => void
|
||||
handleHideGeneratedForm?: () => void
|
||||
formData?: any
|
||||
handleSubmitHumanInputForm?: (data: any) => Promise<void>
|
||||
handleAfterHumanInputStepRun?: () => void
|
||||
} & Partial<SpecialResultPanelProps>
|
||||
|
||||
function formatValue(value: string | any, type: InputVarType) {
|
||||
|
|
@ -73,14 +75,17 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
|
|||
forms,
|
||||
filteredExistVarForms,
|
||||
existVarValuesInForms,
|
||||
generatedFormContentData,
|
||||
showGeneratedForm = false,
|
||||
handleShowGeneratedForm,
|
||||
handleHideGeneratedForm,
|
||||
formData,
|
||||
handleSubmitHumanInputForm,
|
||||
handleAfterHumanInputStepRun,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isHumanInput = nodeType === BlockEnum.HumanInput
|
||||
const showBackButton = filteredExistVarForms.length > 0
|
||||
|
||||
const isFileLoaded = (() => {
|
||||
if (!forms || forms.length === 0)
|
||||
|
|
@ -154,6 +159,11 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
|
|||
onRun(submitData)
|
||||
}
|
||||
|
||||
const handleHumanInputFormSubmit = async (data: any) => {
|
||||
await handleSubmitHumanInputForm?.(data)
|
||||
handleAfterHumanInputStepRun?.()
|
||||
}
|
||||
|
||||
const hasRun = useRef(false)
|
||||
useEffect(() => {
|
||||
// React 18 run twice in dev mode
|
||||
|
|
@ -162,7 +172,9 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
|
|||
hasRun.current = true
|
||||
if (filteredExistVarForms.length === 0 && !isHumanInput)
|
||||
onRun({})
|
||||
}, [filteredExistVarForms, onRun])
|
||||
if (filteredExistVarForms.length === 0 && isHumanInput)
|
||||
handleShowGeneratedForm?.({})
|
||||
}, [filteredExistVarForms, handleShowGeneratedForm, isHumanInput, onRun])
|
||||
|
||||
if (filteredExistVarForms.length === 0 && !isHumanInput)
|
||||
return null
|
||||
|
|
@ -187,14 +199,13 @@ const BeforeRunForm: FC<BeforeRunFormProps> = ({
|
|||
))}
|
||||
</div>
|
||||
)}
|
||||
{showGeneratedForm && generatedFormContentData && (
|
||||
{showGeneratedForm && formData && (
|
||||
<SingleRunForm
|
||||
nodeName={nodeName}
|
||||
showBackButton={showBackButton}
|
||||
handleBack={handleHideGeneratedForm}
|
||||
showBackButton={generatedFormContentData.showBackButton}
|
||||
formContent={generatedFormContentData.formContent}
|
||||
inputFields={generatedFormContentData.inputFields}
|
||||
userActions={generatedFormContentData.userActions}
|
||||
data={formData}
|
||||
onSubmit={handleHumanInputFormSubmit}
|
||||
/>
|
||||
)}
|
||||
{!showGeneratedForm && (
|
||||
|
|
|
|||
|
|
@ -445,6 +445,7 @@ const BasePanel: FC<BasePanelProps> = ({
|
|||
{...passedLogParams}
|
||||
existVarValuesInForms={getExistVarValuesInForms(singleRunParams?.forms as any)}
|
||||
filteredExistVarForms={getFilteredExistVarForms(singleRunParams?.forms as any)}
|
||||
handleAfterHumanInputStepRun={handleAfterCustomSingleRun}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ const useLastRun = <T>({
|
|||
const isLoopNode = blockType === BlockEnum.Loop
|
||||
const isAggregatorNode = blockType === BlockEnum.VariableAggregator
|
||||
const isCustomRunNode = isSupportCustomRunForm(blockType)
|
||||
const isHumanInputNode = blockType === BlockEnum.HumanInput
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const {
|
||||
getData: getDataForCheckMore,
|
||||
|
|
@ -342,17 +343,11 @@ const useLastRun = <T>({
|
|||
return
|
||||
if (blockType === BlockEnum.TriggerWebhook || blockType === BlockEnum.TriggerPlugin || blockType === BlockEnum.TriggerSchedule)
|
||||
setShowVariableInspectPanel(true)
|
||||
if (isCustomRunNode) {
|
||||
if (isCustomRunNode || isHumanInputNode) {
|
||||
showSingleRun()
|
||||
return
|
||||
}
|
||||
const vars = singleRunParams?.getDependentVars?.()
|
||||
// TODO human input
|
||||
if (singleRunParams?.generatedFormContentData) {
|
||||
singleRunParams?.handleShowGeneratedForm()
|
||||
showSingleRun()
|
||||
return
|
||||
}
|
||||
// no need to input params
|
||||
if (isAggregatorNode ? checkAggregatorVarsSet(vars) : isAllVarsHasValue(vars)) {
|
||||
callRunApi({}, async () => {
|
||||
|
|
|
|||
|
|
@ -7,58 +7,32 @@ import { useState } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import ContentItem from '@/app/components/base/chat/chat/answer/human-input-content/content-item'
|
||||
import { UserActionButtonType } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import { getButtonStyle, initializeInputs, splitByOutputVar } from '@/app/components/base/chat/chat/answer/human-input-content/utils'
|
||||
|
||||
type Props = {
|
||||
nodeName: string
|
||||
formContent: string
|
||||
inputFields: FormInputItem[]
|
||||
userActions: UserAction[]
|
||||
data: {
|
||||
form_content: string
|
||||
inputs: FormInputItem[]
|
||||
actions: UserAction[]
|
||||
}
|
||||
showBackButton?: boolean
|
||||
handleBack?: () => void
|
||||
onSubmit?: (data: any) => Promise<void>
|
||||
}
|
||||
|
||||
const FormContent = ({
|
||||
nodeName,
|
||||
formContent,
|
||||
inputFields,
|
||||
userActions,
|
||||
data,
|
||||
showBackButton,
|
||||
handleBack,
|
||||
onSubmit,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const splitByOutputVar = (content: string): string[] => {
|
||||
const outputVarRegex = /(\{\{#\$output\.[^#]+#\}\})/g
|
||||
const parts = content.split(outputVarRegex)
|
||||
return parts.filter(part => part.length > 0)
|
||||
}
|
||||
|
||||
const initializeInputs = (formInputs: FormInputItem[]) => {
|
||||
const initialInputs: Record<string, any> = {}
|
||||
formInputs.forEach((item) => {
|
||||
if (item.type === 'text-input' || item.type === 'paragraph')
|
||||
initialInputs[item.output_variable_name] = ''
|
||||
else
|
||||
initialInputs[item.output_variable_name] = undefined
|
||||
})
|
||||
return initialInputs
|
||||
}
|
||||
|
||||
const contentList = splitByOutputVar(formContent)
|
||||
const defaultInputValues = initializeInputs(inputFields)
|
||||
const [inputs, setInputs] = useState(defaultInputValues)
|
||||
|
||||
const getButtonStyle = (style: UserActionButtonType) => {
|
||||
if (style === UserActionButtonType.Primary)
|
||||
return 'primary'
|
||||
if (style === UserActionButtonType.Default)
|
||||
return 'secondary'
|
||||
if (style === UserActionButtonType.Accent)
|
||||
return 'secondary-accent'
|
||||
if (style === UserActionButtonType.Ghost)
|
||||
return 'ghost'
|
||||
}
|
||||
const defaultInputs = initializeInputs(data.inputs)
|
||||
const contentList = splitByOutputVar(data.form_content)
|
||||
const [inputs, setInputs] = useState(defaultInputs)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
// use immer
|
||||
const handleInputsChange = (name: string, value: any) => {
|
||||
|
|
@ -69,7 +43,9 @@ const FormContent = ({
|
|||
}
|
||||
|
||||
const submit = async (actionID: string) => {
|
||||
// TODO
|
||||
setIsSubmitting(true)
|
||||
await onSubmit?.({ inputs, action: actionID })
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -89,15 +65,16 @@ const FormContent = ({
|
|||
<ContentItem
|
||||
key={index}
|
||||
content={content}
|
||||
formInputFields={inputFields}
|
||||
formInputFields={data.inputs}
|
||||
inputs={inputs}
|
||||
onInputChange={handleInputsChange}
|
||||
/>
|
||||
))}
|
||||
<div className="flex flex-wrap gap-1 py-1">
|
||||
{userActions.map((action: any) => (
|
||||
{data.actions.map((action: any) => (
|
||||
<Button
|
||||
key={action.id}
|
||||
disabled={isSubmitting}
|
||||
variant={getButtonStyle(action.button_style) as any}
|
||||
onClick={() => submit(action.id)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import type { HumanInputNodeType } from '../types'
|
||||
import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { fetchHumanInputNodeStepRunForm, submitHumanInputNodeStepRunForm } from '@/service/workflow'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import useNodeCrud from '../../_base/hooks/use-node-crud'
|
||||
import { isOutput } from '../utils'
|
||||
|
||||
|
|
@ -24,13 +27,13 @@ const useSingleRunFormParams = ({
|
|||
}: Params) => {
|
||||
const { t } = useTranslation()
|
||||
const { inputs } = useNodeCrud<HumanInputNodeType>(id, payload)
|
||||
const [submittedData, setSubmittedData] = useState<Record<string, any> | null>(null)
|
||||
const [showGeneratedForm, setShowGeneratedForm] = useState(false)
|
||||
const [formData, setFormData] = useState<any>(null)
|
||||
const generatedInputs = useMemo(() => {
|
||||
if (!inputs.form_content)
|
||||
return []
|
||||
return getInputVars([inputs.form_content]).filter(item => !isOutput(item.value_selector || []))
|
||||
}, [inputs.form_content])
|
||||
}, [getInputVars, inputs.form_content])
|
||||
|
||||
const forms = useMemo(() => {
|
||||
const forms: FormProps[] = [{
|
||||
|
|
@ -40,22 +43,7 @@ const useSingleRunFormParams = ({
|
|||
onChange: setRunInputData,
|
||||
}]
|
||||
return forms
|
||||
}, [runInputData, setRunInputData, generatedInputs])
|
||||
|
||||
const formContentOutputFields = useMemo(() => {
|
||||
const res = (inputs.inputs || [])
|
||||
.filter((item) => {
|
||||
return inputs.form_content.includes(`{{#$output.${item.output_variable_name}#}}`)
|
||||
})
|
||||
.map((item) => {
|
||||
return {
|
||||
type: item.type,
|
||||
output_variable_name: item.output_variable_name,
|
||||
placeholder: item.placeholder?.type === 'constant' ? item.placeholder.value : '',
|
||||
}
|
||||
})
|
||||
return res
|
||||
}, [inputs.form_content, inputs.inputs])
|
||||
}, [t, generatedInputs, runInputData, setRunInputData])
|
||||
|
||||
const getDependentVars = () => {
|
||||
return generatedInputs.map((item) => {
|
||||
|
|
@ -67,36 +55,34 @@ const useSingleRunFormParams = ({
|
|||
}).filter(arr => arr.length > 0)
|
||||
}
|
||||
|
||||
const generatedFormContentData = useMemo(() => {
|
||||
if (!inputs.form_content)
|
||||
return null
|
||||
if (!generatedInputs.length) {
|
||||
return {
|
||||
formContent: inputs.form_content,
|
||||
inputFields: formContentOutputFields,
|
||||
userActions: inputs.user_actions,
|
||||
showBackButton: false,
|
||||
}
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const appId = appDetail?.id
|
||||
const isWorkflowMode = appDetail?.mode === AppModeEnum.WORKFLOW
|
||||
const fetchURL = useMemo(() => {
|
||||
if (!appId)
|
||||
return ''
|
||||
if (isWorkflowMode) {
|
||||
return `/apps/${appId}/advanced-chat/workflows/draft/human_input/nodes/${id}/form`
|
||||
}
|
||||
else {
|
||||
if (!submittedData)
|
||||
return null
|
||||
const newContent = inputs.form_content.replace(/\{\{#(.*?)#\}\}/g, (originStr, varName) => {
|
||||
if (isOutput(varName.split('.')))
|
||||
return originStr
|
||||
return submittedData[`#${varName}#`] ?? ''
|
||||
})
|
||||
return {
|
||||
formContent: newContent,
|
||||
inputFields: formContentOutputFields,
|
||||
userActions: inputs.user_actions,
|
||||
showBackButton: true,
|
||||
}
|
||||
return `/apps/${appId}/workflows/draft/human_input/nodes/${id}/form`
|
||||
}
|
||||
}, [inputs.form_content, inputs.user_actions, submittedData, formContentOutputFields])
|
||||
}, [appId, id, isWorkflowMode])
|
||||
|
||||
const handleShowGeneratedForm = (formValue: Record<string, any>) => {
|
||||
setSubmittedData(formValue)
|
||||
const handleFetchFormContent = useCallback(async (inputs: Record<string, any>) => {
|
||||
if (!fetchURL)
|
||||
return null
|
||||
const data = await fetchHumanInputNodeStepRunForm(fetchURL, { inputs })
|
||||
setFormData(data)
|
||||
return data
|
||||
}, [fetchURL])
|
||||
|
||||
const handleSubmitHumanInputForm = useCallback(async (formData: any) => {
|
||||
await submitHumanInputNodeStepRunForm(fetchURL, formData)
|
||||
}, [fetchURL])
|
||||
|
||||
const handleShowGeneratedForm = async (formValue: Record<string, any>) => {
|
||||
await handleFetchFormContent(formValue)
|
||||
setShowGeneratedForm(true)
|
||||
}
|
||||
|
||||
|
|
@ -107,10 +93,12 @@ const useSingleRunFormParams = ({
|
|||
return {
|
||||
forms,
|
||||
getDependentVars,
|
||||
generatedFormContentData,
|
||||
showGeneratedForm,
|
||||
handleShowGeneratedForm,
|
||||
handleHideGeneratedForm,
|
||||
formData,
|
||||
handleFetchFormContent,
|
||||
handleSubmitHumanInputForm,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import type { FormInputItem, UserAction } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { CommonResponse } from '@/models/common'
|
||||
import type { FlowType } from '@/types/common'
|
||||
|
|
@ -101,3 +102,26 @@ export const submitHumanInputForm = (token: string, data: {
|
|||
}) => {
|
||||
return post(`/form/human_input/${token}`, { body: data })
|
||||
}
|
||||
|
||||
export const fetchHumanInputNodeStepRunForm = (
|
||||
url: string,
|
||||
params: {
|
||||
inputs: Record<string, any>
|
||||
},
|
||||
) => {
|
||||
return get<{
|
||||
form_content: string
|
||||
inputs: FormInputItem[]
|
||||
user_actions: UserAction[]
|
||||
}>(url, { params })
|
||||
}
|
||||
|
||||
export const submitHumanInputNodeStepRunForm = (
|
||||
url: string,
|
||||
data: {
|
||||
inputs: Record<string, any>
|
||||
action: string
|
||||
},
|
||||
) => {
|
||||
return post<CommonResponse>(url, { body: data })
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue