mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-02-05 01:55:05 +08:00
### What problem does this PR solve? Feat: Add configuration for webhook to the begin node. #10427 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@ -42,9 +42,9 @@ import { FormWrapper } from '../components/form-wrapper';
|
||||
import { Output } from '../components/output';
|
||||
import { PromptEditor } from '../components/prompt-editor';
|
||||
import { QueryVariable } from '../components/query-variable';
|
||||
import { SchemaDialog } from '../components/schema-dialog';
|
||||
import { SchemaPanel } from '../components/schema-panel';
|
||||
import { AgentTools, Agents } from './agent-tools';
|
||||
import { StructuredOutputDialog } from './structured-output-dialog';
|
||||
import { StructuredOutputPanel } from './structured-output-panel';
|
||||
import { useBuildPromptExtraPromptOptions } from './use-build-prompt-options';
|
||||
import {
|
||||
useHandleShowStructuredOutput,
|
||||
@ -327,19 +327,17 @@ function AgentForm({ node }: INextOperatorForm) {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<StructuredOutputPanel
|
||||
value={structuredOutput}
|
||||
></StructuredOutputPanel>
|
||||
<SchemaPanel value={structuredOutput}></SchemaPanel>
|
||||
</section>
|
||||
)}
|
||||
</FormWrapper>
|
||||
</Form>
|
||||
{structuredOutputDialogVisible && (
|
||||
<StructuredOutputDialog
|
||||
<SchemaDialog
|
||||
hideModal={hideStructuredOutputDialog}
|
||||
onOk={handleStructuredOutputDialogOk}
|
||||
initialValues={structuredOutput}
|
||||
></StructuredOutputDialog>
|
||||
></SchemaDialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -12,6 +12,7 @@ import { RAGFlowSelect } from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { FormTooltip } from '@/components/ui/tooltip';
|
||||
import { WebhookAlgorithmList } from '@/constants/agent';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { t } from 'i18next';
|
||||
import { Plus } from 'lucide-react';
|
||||
@ -24,37 +25,71 @@ import { INextOperatorForm } from '../../interface';
|
||||
import { ParameterDialog } from './parameter-dialog';
|
||||
import { QueryTable } from './query-table';
|
||||
import { useEditQueryRecord } from './use-edit-query';
|
||||
import { useHandleModeChange } from './use-handle-mode-change';
|
||||
import { useValues } from './use-values';
|
||||
import { useWatchFormChange } from './use-watch-change';
|
||||
import { WebHook } from './webhook';
|
||||
|
||||
const ModeOptions = [
|
||||
{ value: AgentDialogueMode.Conversational, label: t('flow.conversational') },
|
||||
{ value: AgentDialogueMode.Task, label: t('flow.task') },
|
||||
{ value: AgentDialogueMode.Webhook, label: t('flow.webhook.name') },
|
||||
];
|
||||
|
||||
const FormSchema = z.object({
|
||||
enablePrologue: z.boolean().optional(),
|
||||
prologue: z.string().trim().optional(),
|
||||
mode: z.string(),
|
||||
inputs: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
type: z.string(),
|
||||
value: z.string(),
|
||||
optional: z.boolean(),
|
||||
name: z.string(),
|
||||
options: z.array(z.union([z.number(), z.string(), z.boolean()])),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
methods: z.string().optional(),
|
||||
content_types: z.string().optional(),
|
||||
security: z
|
||||
.object({
|
||||
auth_type: z.string(),
|
||||
ip_whitelist: z.array(z.object({ value: z.string() })),
|
||||
rate_limit: z.object({
|
||||
limit: z.number(),
|
||||
per: z.string().optional(),
|
||||
}),
|
||||
max_body_size: z.string(),
|
||||
jwt: z
|
||||
.object({
|
||||
algorithm: z.string().default(WebhookAlgorithmList[0]).optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
schema: z.record(z.any()).optional(),
|
||||
response: z
|
||||
.object({
|
||||
status: z.number(),
|
||||
headers_template: z.array(
|
||||
z.object({ key: z.string(), value: z.string() }),
|
||||
),
|
||||
body_template: z.array(z.object({ key: z.string(), value: z.string() })),
|
||||
})
|
||||
.optional(),
|
||||
execution_mode: z.string().optional(),
|
||||
});
|
||||
|
||||
export type BeginFormSchemaType = z.infer<typeof FormSchema>;
|
||||
|
||||
function BeginForm({ node }: INextOperatorForm) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const values = useValues(node);
|
||||
|
||||
const FormSchema = z.object({
|
||||
enablePrologue: z.boolean().optional(),
|
||||
prologue: z.string().trim().optional(),
|
||||
mode: z.string(),
|
||||
inputs: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
type: z.string(),
|
||||
value: z.string(),
|
||||
optional: z.boolean(),
|
||||
name: z.string(),
|
||||
options: z.array(z.union([z.number(), z.string(), z.boolean()])),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: values,
|
||||
resolver: zodResolver(FormSchema),
|
||||
@ -72,6 +107,8 @@ function BeginForm({ node }: INextOperatorForm) {
|
||||
|
||||
const previousModeRef = useRef(mode);
|
||||
|
||||
const { handleModeChange } = useHandleModeChange(form);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
previousModeRef.current === AgentDialogueMode.Task &&
|
||||
@ -111,6 +148,10 @@ function BeginForm({ node }: INextOperatorForm) {
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={ModeOptions}
|
||||
{...field}
|
||||
onChange={(val) => {
|
||||
handleModeChange(val);
|
||||
field.onChange(val);
|
||||
}}
|
||||
></RAGFlowSelect>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@ -158,44 +199,49 @@ function BeginForm({ node }: INextOperatorForm) {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{/* Create a hidden field to make Form instance record this */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'inputs'}
|
||||
render={() => <div></div>}
|
||||
/>
|
||||
<Collapse
|
||||
title={
|
||||
<div>
|
||||
{t('flow.input')}
|
||||
<FormTooltip tooltip={t('flow.beginInputTip')}></FormTooltip>
|
||||
</div>
|
||||
}
|
||||
rightContent={
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
showModal();
|
||||
}}
|
||||
{mode === AgentDialogueMode.Webhook && <WebHook></WebHook>}
|
||||
{mode !== AgentDialogueMode.Webhook && (
|
||||
<>
|
||||
{/* Create a hidden field to make Form instance record this */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={'inputs'}
|
||||
render={() => <div></div>}
|
||||
/>
|
||||
<Collapse
|
||||
title={
|
||||
<div>
|
||||
{t('flow.input')}
|
||||
<FormTooltip tooltip={t('flow.beginInputTip')}></FormTooltip>
|
||||
</div>
|
||||
}
|
||||
rightContent={
|
||||
<Button
|
||||
variant={'ghost'}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
showModal();
|
||||
}}
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Plus />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<QueryTable
|
||||
data={inputs}
|
||||
showModal={showModal}
|
||||
deleteRecord={handleDeleteRecord}
|
||||
></QueryTable>
|
||||
</Collapse>
|
||||
{visible && (
|
||||
<ParameterDialog
|
||||
hideModal={hideModal}
|
||||
initialValue={currentRecord}
|
||||
otherThanCurrentQuery={otherThanCurrentQuery}
|
||||
submit={ok}
|
||||
></ParameterDialog>
|
||||
<QueryTable
|
||||
data={inputs}
|
||||
showModal={showModal}
|
||||
deleteRecord={handleDeleteRecord}
|
||||
></QueryTable>
|
||||
</Collapse>
|
||||
{visible && (
|
||||
<ParameterDialog
|
||||
hideModal={hideModal}
|
||||
initialValue={currentRecord}
|
||||
otherThanCurrentQuery={otherThanCurrentQuery}
|
||||
submit={ok}
|
||||
></ParameterDialog>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</section>
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
import { useCallback } from 'react';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import {
|
||||
AgentDialogueMode,
|
||||
RateLimitPerList,
|
||||
WebhookExecutionMode,
|
||||
WebhookMaxBodySize,
|
||||
WebhookSecurityAuthType,
|
||||
} from '../../constant';
|
||||
|
||||
// const WebhookSchema = {
|
||||
// query: {
|
||||
// type: 'object',
|
||||
// required: [],
|
||||
// properties: {
|
||||
// // debug: { type: 'boolean' },
|
||||
// // event: { type: 'string' },
|
||||
// },
|
||||
// },
|
||||
|
||||
// headers: {
|
||||
// type: 'object',
|
||||
// required: [],
|
||||
// properties: {
|
||||
// // 'X-Trace-ID': { type: 'string' },
|
||||
// },
|
||||
// },
|
||||
|
||||
// body: {
|
||||
// type: 'object',
|
||||
// required: [],
|
||||
// properties: {
|
||||
// id: { type: 'string' },
|
||||
// payload: { type: 'object' },
|
||||
// },
|
||||
// },
|
||||
// };
|
||||
|
||||
const schema = {
|
||||
properties: {
|
||||
query: {
|
||||
type: 'object',
|
||||
description: '',
|
||||
},
|
||||
headers: {
|
||||
type: 'object',
|
||||
description: '',
|
||||
},
|
||||
body: {
|
||||
type: 'object',
|
||||
description: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const initialFormValuesMap = {
|
||||
schema: schema,
|
||||
'security.auth_type': WebhookSecurityAuthType.Basic,
|
||||
'security.rate_limit.per': RateLimitPerList[0],
|
||||
'security.max_body_size': WebhookMaxBodySize[0],
|
||||
execution_mode: WebhookExecutionMode.Immediately,
|
||||
};
|
||||
|
||||
export function useHandleModeChange(form: UseFormReturn<any>) {
|
||||
const handleModeChange = useCallback(
|
||||
(mode: AgentDialogueMode) => {
|
||||
if (mode === AgentDialogueMode.Webhook) {
|
||||
Object.entries(initialFormValuesMap).forEach(([key, value]) => {
|
||||
form.setValue(key, value, { shouldDirty: true });
|
||||
});
|
||||
}
|
||||
},
|
||||
[form],
|
||||
);
|
||||
return { handleModeChange };
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
import { JSONSchema } from '@/components/jsonjoy-builder';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { useCallback } from 'react';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
|
||||
export function useShowSchemaDialog(form: UseFormReturn<any>) {
|
||||
const {
|
||||
visible: schemaDialogVisible,
|
||||
showModal: showSchemaDialog,
|
||||
hideModal: hideSchemaDialog,
|
||||
} = useSetModalState();
|
||||
|
||||
const handleSchemaDialogOk = useCallback(
|
||||
(values: JSONSchema) => {
|
||||
// Sync data to canvas
|
||||
form.setValue('schema', values);
|
||||
hideSchemaDialog();
|
||||
},
|
||||
[form, hideSchemaDialog],
|
||||
);
|
||||
|
||||
return {
|
||||
schemaDialogVisible,
|
||||
showSchemaDialog,
|
||||
hideSchemaDialog,
|
||||
handleSchemaDialogOk,
|
||||
};
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { omit } from 'lodash';
|
||||
import { useEffect } from 'react';
|
||||
import { UseFormReturn, useWatch } from 'react-hook-form';
|
||||
import { AgentDialogueMode } from '../../constant';
|
||||
import { BeginQuery } from '../../interface';
|
||||
import useGraphStore from '../../store';
|
||||
|
||||
@ -20,9 +21,21 @@ export function useWatchFormChange(id?: string, form?: UseFormReturn) {
|
||||
if (id) {
|
||||
values = form?.getValues() || {};
|
||||
|
||||
let outputs: Record<string, any> = {};
|
||||
|
||||
// For webhook mode, use schema properties as direct outputs
|
||||
// Each property (body, headers, query) should be able to show secondary menu
|
||||
if (
|
||||
values.mode === AgentDialogueMode.Webhook &&
|
||||
values.schema?.properties
|
||||
) {
|
||||
outputs = { ...values.schema.properties };
|
||||
}
|
||||
|
||||
const nextValues = {
|
||||
...values,
|
||||
inputs: transferInputsArrayToObject(values.inputs),
|
||||
outputs,
|
||||
};
|
||||
|
||||
updateNodeForm(id, nextValues);
|
||||
|
||||
139
web/src/pages/agent/form/begin-form/webhook/auth.tsx
Normal file
139
web/src/pages/agent/form/begin-form/webhook/auth.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { WebhookAlgorithmList } from '@/constants/agent';
|
||||
import { WebhookSecurityAuthType } from '@/pages/agent/constant';
|
||||
import { buildOptions } from '@/utils/form';
|
||||
import { useCallback } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const AlgorithmOptions = buildOptions(WebhookAlgorithmList);
|
||||
|
||||
const RequiredClaimsOptions = buildOptions(['exp', 'sub']);
|
||||
|
||||
export function Auth() {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
|
||||
const authType = useWatch({
|
||||
name: 'security.auth_type',
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
const renderTokenAuth = useCallback(
|
||||
() => (
|
||||
<>
|
||||
<RAGFlowFormItem
|
||||
name="security.token.token_header"
|
||||
label={t('flow.webhook.tokenHeader')}
|
||||
>
|
||||
<Input></Input>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.token.token_value"
|
||||
label={t('flow.webhook.tokenValue')}
|
||||
>
|
||||
<Input></Input>
|
||||
</RAGFlowFormItem>
|
||||
</>
|
||||
),
|
||||
[t],
|
||||
);
|
||||
|
||||
const renderBasicAuth = useCallback(
|
||||
() => (
|
||||
<>
|
||||
<RAGFlowFormItem
|
||||
name="security.basic_auth.username"
|
||||
label={t('flow.webhook.username')}
|
||||
>
|
||||
<Input></Input>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.basic_auth.password"
|
||||
label={t('flow.webhook.password')}
|
||||
>
|
||||
<Input></Input>
|
||||
</RAGFlowFormItem>
|
||||
</>
|
||||
),
|
||||
[t],
|
||||
);
|
||||
|
||||
const renderJwtAuth = useCallback(
|
||||
() => (
|
||||
<>
|
||||
<RAGFlowFormItem
|
||||
name="security.jwt.algorithm"
|
||||
label={t('flow.webhook.algorithm')}
|
||||
>
|
||||
<SelectWithSearch options={AlgorithmOptions}></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.jwt.secret"
|
||||
label={t('flow.webhook.secret')}
|
||||
>
|
||||
<Input></Input>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.jwt.issuer"
|
||||
label={t('flow.webhook.issuer')}
|
||||
>
|
||||
<Input></Input>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.jwt.audience"
|
||||
label={t('flow.webhook.audience')}
|
||||
>
|
||||
<Input></Input>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.jwt.required_claims"
|
||||
label={t('flow.webhook.requiredClaims')}
|
||||
>
|
||||
<SelectWithSearch options={RequiredClaimsOptions}></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
</>
|
||||
),
|
||||
[t],
|
||||
);
|
||||
|
||||
const renderHmacAuth = useCallback(
|
||||
() => (
|
||||
<>
|
||||
<RAGFlowFormItem
|
||||
name="security.hmac.header"
|
||||
label={t('flow.webhook.header')}
|
||||
>
|
||||
<Input></Input>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.hmac.secret"
|
||||
label={t('flow.webhook.secret')}
|
||||
>
|
||||
<Input></Input>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.hmac.algorithm"
|
||||
label={t('flow.webhook.algorithm')}
|
||||
>
|
||||
<SelectWithSearch options={AlgorithmOptions}></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
</>
|
||||
),
|
||||
[t],
|
||||
);
|
||||
|
||||
const AuthMap = {
|
||||
[WebhookSecurityAuthType.Token]: renderTokenAuth,
|
||||
[WebhookSecurityAuthType.Basic]: renderBasicAuth,
|
||||
[WebhookSecurityAuthType.Jwt]: renderJwtAuth,
|
||||
[WebhookSecurityAuthType.Hmac]: renderHmacAuth,
|
||||
[WebhookSecurityAuthType.None]: () => null,
|
||||
};
|
||||
|
||||
return AuthMap[
|
||||
(authType ?? WebhookSecurityAuthType.None) as WebhookSecurityAuthType
|
||||
]();
|
||||
}
|
||||
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 = 'key',
|
||||
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>
|
||||
);
|
||||
}
|
||||
134
web/src/pages/agent/form/begin-form/webhook/index.tsx
Normal file
134
web/src/pages/agent/form/begin-form/webhook/index.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import { Collapse } from '@/components/collapse';
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
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 { buildOptions } from '@/utils/form';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
RateLimitPerList,
|
||||
WebhookContentType,
|
||||
WebhookExecutionMode,
|
||||
WebhookMaxBodySize,
|
||||
WebhookMethod,
|
||||
WebhookSecurityAuthType,
|
||||
} from '../../../constant';
|
||||
import { DynamicStringForm } from '../../components/dynamic-string-form';
|
||||
import { SchemaDialog } from '../../components/schema-dialog';
|
||||
import { SchemaPanel } from '../../components/schema-panel';
|
||||
import { useShowSchemaDialog } from '../use-show-schema-dialog';
|
||||
import { Auth } from './auth';
|
||||
import { WebhookResponse } from './response';
|
||||
|
||||
const RateLimitPerOptions = buildOptions(RateLimitPerList);
|
||||
|
||||
export function WebHook() {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
|
||||
const executionMode = useWatch({
|
||||
control: form.control,
|
||||
name: 'execution_mode',
|
||||
});
|
||||
|
||||
const {
|
||||
showSchemaDialog,
|
||||
schemaDialogVisible,
|
||||
hideSchemaDialog,
|
||||
handleSchemaDialogOk,
|
||||
} = useShowSchemaDialog(form);
|
||||
|
||||
const schema = form.getValues('schema');
|
||||
|
||||
return (
|
||||
<>
|
||||
<RAGFlowFormItem name="methods" label={t('flow.webhook.methods')}>
|
||||
<SelectWithSearch
|
||||
options={buildOptions(WebhookMethod)}
|
||||
></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="content_types"
|
||||
label={t('flow.webhook.contentTypes')}
|
||||
>
|
||||
<SelectWithSearch
|
||||
options={buildOptions(WebhookContentType)}
|
||||
></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
<Collapse title={<div>Security</div>}>
|
||||
<section className="space-y-4">
|
||||
<RAGFlowFormItem
|
||||
name="security.auth_type"
|
||||
label={t('flow.webhook.authType')}
|
||||
>
|
||||
<SelectWithSearch
|
||||
options={buildOptions(WebhookSecurityAuthType)}
|
||||
></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
<Auth></Auth>
|
||||
<RAGFlowFormItem
|
||||
name="security.rate_limit.limit"
|
||||
label={t('flow.webhook.limit')}
|
||||
>
|
||||
<Input type="number"></Input>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.rate_limit.per"
|
||||
label={t('flow.webhook.per')}
|
||||
>
|
||||
<SelectWithSearch options={RateLimitPerOptions}></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="security.max_body_size"
|
||||
label={t('flow.webhook.maxBodySize')}
|
||||
>
|
||||
<SelectWithSearch
|
||||
options={buildOptions(WebhookMaxBodySize)}
|
||||
></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
<DynamicStringForm
|
||||
name="security.ip_whitelist"
|
||||
label={t('flow.webhook.ipWhitelist')}
|
||||
></DynamicStringForm>
|
||||
</section>
|
||||
</Collapse>
|
||||
<RAGFlowFormItem
|
||||
name="schema"
|
||||
label={t('flow.webhook.schema')}
|
||||
className="hidden"
|
||||
>
|
||||
<Textarea></Textarea>
|
||||
</RAGFlowFormItem>
|
||||
<RAGFlowFormItem
|
||||
name="execution_mode"
|
||||
label={t('flow.webhook.executionMode')}
|
||||
>
|
||||
<SelectWithSearch
|
||||
options={buildOptions(WebhookExecutionMode)}
|
||||
></SelectWithSearch>
|
||||
</RAGFlowFormItem>
|
||||
{executionMode === WebhookExecutionMode.Immediately && (
|
||||
<WebhookResponse></WebhookResponse>
|
||||
)}
|
||||
<Separator></Separator>
|
||||
<section className="flex justify-between items-center">
|
||||
Schema
|
||||
<Button variant={'ghost'} onClick={showSchemaDialog}>
|
||||
{t('flow.structuredOutput.configuration')}
|
||||
</Button>
|
||||
</section>
|
||||
<SchemaPanel value={schema}></SchemaPanel>
|
||||
{schemaDialogVisible && (
|
||||
<SchemaDialog
|
||||
initialValues={schema}
|
||||
hideModal={hideSchemaDialog}
|
||||
onOk={handleSchemaDialogOk}
|
||||
pattern={''}
|
||||
></SchemaDialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
30
web/src/pages/agent/form/begin-form/webhook/response.tsx
Normal file
30
web/src/pages/agent/form/begin-form/webhook/response.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { Collapse } from '@/components/collapse';
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DynamicResponse } from './dynamic-response';
|
||||
|
||||
export function WebhookResponse() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Collapse title={<div>Response</div>}>
|
||||
<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>
|
||||
</Collapse>
|
||||
);
|
||||
}
|
||||
46
web/src/pages/agent/form/components/dynamic-string-form.tsx
Normal file
46
web/src/pages/agent/form/components/dynamic-string-form.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { RAGFlowFormItem } from '@/components/ragflow-form';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { DynamicFormHeader, FormListHeaderProps } from './dynamic-fom-header';
|
||||
|
||||
type DynamicStringFormProps = { name: string } & FormListHeaderProps;
|
||||
export function DynamicStringForm({ name, label }: DynamicStringFormProps) {
|
||||
const form = useFormContext();
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name: name,
|
||||
control: form.control,
|
||||
});
|
||||
|
||||
return (
|
||||
<section>
|
||||
<DynamicFormHeader
|
||||
label={label}
|
||||
onClick={() => append({ value: '' })}
|
||||
></DynamicFormHeader>
|
||||
<div className="space-y-4">
|
||||
{fields.map((field, index) => (
|
||||
<div key={field.id} className="flex items-center gap-2">
|
||||
<RAGFlowFormItem
|
||||
name={`${name}.${index}.value`}
|
||||
label="delimiter"
|
||||
labelClassName="!hidden"
|
||||
className="flex-1 !m-0"
|
||||
>
|
||||
<Input className="!m-0"></Input>
|
||||
</RAGFlowFormItem>
|
||||
<Button
|
||||
type="button"
|
||||
variant={'ghost'}
|
||||
onClick={() => remove(index)}
|
||||
>
|
||||
<Trash2 />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@ -3,6 +3,7 @@ import {
|
||||
JsonSchemaVisualizer,
|
||||
SchemaVisualEditor,
|
||||
} from '@/components/jsonjoy-builder';
|
||||
import { KeyInputProps } from '@/components/jsonjoy-builder/components/schema-editor/interface';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
@ -16,11 +17,12 @@ import { IModalProps } from '@/interfaces/common';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function StructuredOutputDialog({
|
||||
export function SchemaDialog({
|
||||
hideModal,
|
||||
onOk,
|
||||
initialValues,
|
||||
}: IModalProps<any>) {
|
||||
pattern,
|
||||
}: IModalProps<any> & KeyInputProps) {
|
||||
const { t } = useTranslation();
|
||||
const [schema, setSchema] = useState<JSONSchema>(initialValues);
|
||||
|
||||
@ -36,7 +38,11 @@ export function StructuredOutputDialog({
|
||||
</DialogHeader>
|
||||
<section className="flex overflow-auto">
|
||||
<div className="flex-1">
|
||||
<SchemaVisualEditor schema={schema} onChange={setSchema} />
|
||||
<SchemaVisualEditor
|
||||
schema={schema}
|
||||
onChange={setSchema}
|
||||
pattern={pattern}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<JsonSchemaVisualizer schema={schema} onChange={setSchema} />
|
||||
@ -1,6 +1,6 @@
|
||||
import { JSONSchema, JsonSchemaVisualizer } from '@/components/jsonjoy-builder';
|
||||
|
||||
export function StructuredOutputPanel({ value }: { value: JSONSchema }) {
|
||||
export function SchemaPanel({ value }: { value: JSONSchema }) {
|
||||
return (
|
||||
<section className="h-48">
|
||||
<JsonSchemaVisualizer
|
||||
Reference in New Issue
Block a user