Feat: Add dynamic webhook response.
This commit is contained in:
parent
f317c5116b
commit
137de4849d
4 changed files with 260 additions and 6 deletions
|
|
@ -68,7 +68,7 @@ function BeginForm({ node }: INextOperatorForm) {
|
||||||
max_body_size: z.string(),
|
max_body_size: z.string(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
schema: z.string().optional(),
|
schema: z.record(z.any()).optional(),
|
||||||
response: z
|
response: z
|
||||||
.object({
|
.object({
|
||||||
status: z.number(),
|
status: z.number(),
|
||||||
|
|
|
||||||
213
web/src/pages/agent/form/begin-form/webhook/dynamic-response.tsx
Normal file
213
web/src/pages/agent/form/begin-form/webhook/dynamic-response.tsx
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
import { BoolSegmented } from '@/components/bool-segmented';
|
||||||
|
import { KeyInput } from '@/components/key-input';
|
||||||
|
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||||
|
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||||
|
import { useIsDarkTheme } from '@/components/theme-provider';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Editor, loader } from '@monaco-editor/react';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
import { ReactNode, useCallback } from 'react';
|
||||||
|
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||||
|
import { InputMode, TypesWithArray } from '../../../constant';
|
||||||
|
import {
|
||||||
|
InputModeOptions,
|
||||||
|
buildConversationVariableSelectOptions,
|
||||||
|
} from '../../../utils';
|
||||||
|
import { DynamicFormHeader } from '../../components/dynamic-fom-header';
|
||||||
|
import { QueryVariable } from '../../components/query-variable';
|
||||||
|
|
||||||
|
loader.config({ paths: { vs: '/vs' } });
|
||||||
|
|
||||||
|
type SelectKeysProps = {
|
||||||
|
name: string;
|
||||||
|
label: ReactNode;
|
||||||
|
tooltip?: string;
|
||||||
|
keyField?: string;
|
||||||
|
valueField?: string;
|
||||||
|
operatorField?: string;
|
||||||
|
nodeId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const VariableTypeOptions = buildConversationVariableSelectOptions();
|
||||||
|
|
||||||
|
const modeField = 'input_mode';
|
||||||
|
|
||||||
|
const ConstantValueMap = {
|
||||||
|
[TypesWithArray.Boolean]: true,
|
||||||
|
[TypesWithArray.Number]: 0,
|
||||||
|
[TypesWithArray.String]: '',
|
||||||
|
[TypesWithArray.ArrayBoolean]: '[]',
|
||||||
|
[TypesWithArray.ArrayNumber]: '[]',
|
||||||
|
[TypesWithArray.ArrayString]: '[]',
|
||||||
|
[TypesWithArray.ArrayObject]: '[]',
|
||||||
|
[TypesWithArray.Object]: '{}',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DynamicResponse({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
tooltip,
|
||||||
|
keyField = 'variable',
|
||||||
|
valueField = 'value',
|
||||||
|
operatorField = 'type',
|
||||||
|
}: SelectKeysProps) {
|
||||||
|
const form = useFormContext();
|
||||||
|
const isDarkTheme = useIsDarkTheme();
|
||||||
|
|
||||||
|
const { fields, remove, append } = useFieldArray({
|
||||||
|
name: name,
|
||||||
|
control: form.control,
|
||||||
|
});
|
||||||
|
|
||||||
|
const initializeValue = useCallback(
|
||||||
|
(mode: string, variableType: string, valueFieldAlias: string) => {
|
||||||
|
if (mode === InputMode.Variable) {
|
||||||
|
form.setValue(valueFieldAlias, '', { shouldDirty: true });
|
||||||
|
} else {
|
||||||
|
const val = ConstantValueMap[variableType as TypesWithArray];
|
||||||
|
form.setValue(valueFieldAlias, val, { shouldDirty: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[form],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleModeChange = useCallback(
|
||||||
|
(mode: string, valueFieldAlias: string, operatorFieldAlias: string) => {
|
||||||
|
const variableType = form.getValues(operatorFieldAlias);
|
||||||
|
initializeValue(mode, variableType, valueFieldAlias);
|
||||||
|
},
|
||||||
|
[form, initializeValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleVariableTypeChange = useCallback(
|
||||||
|
(variableType: string, valueFieldAlias: string, modeFieldAlias: string) => {
|
||||||
|
const mode = form.getValues(modeFieldAlias);
|
||||||
|
|
||||||
|
initializeValue(mode, variableType, valueFieldAlias);
|
||||||
|
},
|
||||||
|
[form, initializeValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderParameter = useCallback(
|
||||||
|
(operatorFieldName: string, modeFieldName: string) => {
|
||||||
|
const mode = form.getValues(modeFieldName);
|
||||||
|
const logicalOperator = form.getValues(operatorFieldName);
|
||||||
|
|
||||||
|
if (mode === InputMode.Constant) {
|
||||||
|
if (logicalOperator === TypesWithArray.Boolean) {
|
||||||
|
return <BoolSegmented></BoolSegmented>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logicalOperator === TypesWithArray.Number) {
|
||||||
|
return <Input className="w-full" type="number"></Input>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logicalOperator === TypesWithArray.String) {
|
||||||
|
return <Textarea></Textarea>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Editor
|
||||||
|
height={300}
|
||||||
|
theme={isDarkTheme ? 'vs-dark' : 'vs'}
|
||||||
|
language={'json'}
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: false },
|
||||||
|
automaticLayout: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryVariable
|
||||||
|
types={[logicalOperator]}
|
||||||
|
hideLabel
|
||||||
|
pureQuery
|
||||||
|
></QueryVariable>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[form, isDarkTheme],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="space-y-2">
|
||||||
|
<DynamicFormHeader
|
||||||
|
label={label}
|
||||||
|
tooltip={tooltip}
|
||||||
|
onClick={() =>
|
||||||
|
append({
|
||||||
|
[keyField]: '',
|
||||||
|
[valueField]: '',
|
||||||
|
[modeField]: InputMode.Constant,
|
||||||
|
[operatorField]: TypesWithArray.String,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
></DynamicFormHeader>
|
||||||
|
<div className="space-y-5">
|
||||||
|
{fields.map((field, index) => {
|
||||||
|
const keyFieldAlias = `${name}.${index}.${keyField}`;
|
||||||
|
const valueFieldAlias = `${name}.${index}.${valueField}`;
|
||||||
|
const operatorFieldAlias = `${name}.${index}.${operatorField}`;
|
||||||
|
const modeFieldAlias = `${name}.${index}.${modeField}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section key={field.id} className="flex gap-2">
|
||||||
|
<div className="flex-1 space-y-3 min-w-0">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<RAGFlowFormItem name={keyFieldAlias} className="flex-1 ">
|
||||||
|
<KeyInput></KeyInput>
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
<Separator className="w-2" />
|
||||||
|
<RAGFlowFormItem name={operatorFieldAlias} className="flex-1">
|
||||||
|
{(field) => (
|
||||||
|
<SelectWithSearch
|
||||||
|
value={field.value}
|
||||||
|
onChange={(val) => {
|
||||||
|
handleVariableTypeChange(
|
||||||
|
val,
|
||||||
|
valueFieldAlias,
|
||||||
|
modeFieldAlias,
|
||||||
|
);
|
||||||
|
field.onChange(val);
|
||||||
|
}}
|
||||||
|
options={VariableTypeOptions}
|
||||||
|
></SelectWithSearch>
|
||||||
|
)}
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
<Separator className="w-2" />
|
||||||
|
<RAGFlowFormItem name={modeFieldAlias} className="flex-1">
|
||||||
|
{(field) => (
|
||||||
|
<SelectWithSearch
|
||||||
|
value={field.value}
|
||||||
|
onChange={(val) => {
|
||||||
|
handleModeChange(
|
||||||
|
val,
|
||||||
|
valueFieldAlias,
|
||||||
|
operatorFieldAlias,
|
||||||
|
);
|
||||||
|
field.onChange(val);
|
||||||
|
}}
|
||||||
|
options={InputModeOptions}
|
||||||
|
></SelectWithSearch>
|
||||||
|
)}
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
</div>
|
||||||
|
<RAGFlowFormItem name={valueFieldAlias} className="w-full">
|
||||||
|
{renderParameter(operatorFieldAlias, modeFieldAlias)}
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant={'ghost'} onClick={() => remove(index)}>
|
||||||
|
<X />
|
||||||
|
</Button>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { buildOptions } from '@/utils/form';
|
import { buildOptions } from '@/utils/form';
|
||||||
|
import { useFormContext, useWatch } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
WebhookContentType,
|
WebhookContentType,
|
||||||
|
|
@ -12,11 +14,18 @@ import {
|
||||||
} from '../../../constant';
|
} from '../../../constant';
|
||||||
import { DynamicStringForm } from '../../components/dynamic-string-form';
|
import { DynamicStringForm } from '../../components/dynamic-string-form';
|
||||||
import { Auth } from './auth';
|
import { Auth } from './auth';
|
||||||
|
import { WebhookResponse } from './response';
|
||||||
|
|
||||||
const RateLimitPerOptions = buildOptions(['minute', 'hour', 'day']);
|
const RateLimitPerOptions = buildOptions(['minute', 'hour', 'day']);
|
||||||
|
|
||||||
export function WebHook() {
|
export function WebHook() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const form = useFormContext();
|
||||||
|
|
||||||
|
const executionMode = useWatch({
|
||||||
|
control: form.control,
|
||||||
|
name: 'execution_mode',
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -33,7 +42,8 @@ export function WebHook() {
|
||||||
options={buildOptions(WebhookContentType)}
|
options={buildOptions(WebhookContentType)}
|
||||||
></SelectWithSearch>
|
></SelectWithSearch>
|
||||||
</RAGFlowFormItem>
|
</RAGFlowFormItem>
|
||||||
<section className="space-y-5 bg-bg-card p-2 rounded">
|
<Separator></Separator>
|
||||||
|
<>
|
||||||
<RAGFlowFormItem
|
<RAGFlowFormItem
|
||||||
name="security.auth_type"
|
name="security.auth_type"
|
||||||
label={t('flow.webhook.authType')}
|
label={t('flow.webhook.authType')}
|
||||||
|
|
@ -65,13 +75,10 @@ export function WebHook() {
|
||||||
name="security.ip_whitelist"
|
name="security.ip_whitelist"
|
||||||
label={t('flow.webhook.ipWhitelist')}
|
label={t('flow.webhook.ipWhitelist')}
|
||||||
></DynamicStringForm>
|
></DynamicStringForm>
|
||||||
</section>
|
</>
|
||||||
<RAGFlowFormItem name="schema" label={t('flow.webhook.schema')}>
|
<RAGFlowFormItem name="schema" label={t('flow.webhook.schema')}>
|
||||||
<Textarea></Textarea>
|
<Textarea></Textarea>
|
||||||
</RAGFlowFormItem>
|
</RAGFlowFormItem>
|
||||||
<RAGFlowFormItem name="response" label={t('flow.webhook.response')}>
|
|
||||||
<Textarea></Textarea>
|
|
||||||
</RAGFlowFormItem>
|
|
||||||
<RAGFlowFormItem
|
<RAGFlowFormItem
|
||||||
name="execution_mode"
|
name="execution_mode"
|
||||||
label={t('flow.webhook.executionMode')}
|
label={t('flow.webhook.executionMode')}
|
||||||
|
|
@ -80,6 +87,9 @@ export function WebHook() {
|
||||||
options={buildOptions(WebhookExecutionMode)}
|
options={buildOptions(WebhookExecutionMode)}
|
||||||
></SelectWithSearch>
|
></SelectWithSearch>
|
||||||
</RAGFlowFormItem>
|
</RAGFlowFormItem>
|
||||||
|
{executionMode === WebhookExecutionMode.Immediately && (
|
||||||
|
<WebhookResponse></WebhookResponse>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
web/src/pages/agent/form/begin-form/webhook/response.tsx
Normal file
31
web/src/pages/agent/form/begin-form/webhook/response.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { DynamicResponse } from './dynamic-response';
|
||||||
|
|
||||||
|
export function WebhookResponse() {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Separator></Separator>
|
||||||
|
<section className="space-y-4">
|
||||||
|
<RAGFlowFormItem
|
||||||
|
name={'response.status'}
|
||||||
|
label={t('flow.webhook.status')}
|
||||||
|
>
|
||||||
|
<Input type="number"></Input>
|
||||||
|
</RAGFlowFormItem>
|
||||||
|
<DynamicResponse
|
||||||
|
name="response.headers_template"
|
||||||
|
label={t('flow.webhook.headersTemplate')}
|
||||||
|
></DynamicResponse>
|
||||||
|
<DynamicResponse
|
||||||
|
name="response.body_template"
|
||||||
|
label={t('flow.webhook.bodyTemplate')}
|
||||||
|
></DynamicResponse>
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue