浏览代码

Feat: Add RunSheet component #3221 (#8045)

### What problem does this PR solve?

Feat: Add RunSheet component #3221
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
tags/v0.19.1
balibabu 5 个月前
父节点
当前提交
8445143359
没有帐户链接到提交者的电子邮件

+ 10
- 10
web/src/pages/agent/canvas/index.tsx 查看文件

@@ -5,7 +5,7 @@ import {
ReactFlow,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
// import ChatDrawer from '../chat/drawer';
import { ChatSheet } from '../chat/chat-sheet';
import FormSheet from '../form-sheet/next';
import {
useHandleDrop,
@@ -15,7 +15,7 @@ import {
} from '../hooks';
import { useBeforeDelete } from '../hooks/use-before-delete';
import { useShowDrawer } from '../hooks/use-show-drawer';
// import RunDrawer from '../run-drawer';
import RunSheet from '../run-sheet';
import { ButtonEdge } from './edge';
import styles from './index.less';
import { RagNode } from './node';
@@ -66,7 +66,7 @@ interface IProps {
hideDrawer(): void;
}

function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
const {
nodes,
edges,
@@ -165,21 +165,21 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
showSingleDebugDrawer={showSingleDebugDrawer}
></FormSheet>
)}
{/* {chatVisible && (
<ChatDrawer
{chatVisible && (
<ChatSheet
visible={chatVisible}
hideModal={hideRunOrChatDrawer}
></ChatDrawer>
></ChatSheet>
)}

{runVisible && (
<RunDrawer
<RunSheet
hideModal={hideRunOrChatDrawer}
showModal={showChatModal}
></RunDrawer>
)} */}
></RunSheet>
)}
</div>
);
}

export default FlowCanvas;
export default AgentCanvas;

+ 26
- 0
web/src/pages/agent/chat/chat-sheet.tsx 查看文件

@@ -0,0 +1,26 @@
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet';
import { IModalProps } from '@/interfaces/common';

export function ChatSheet({ visible }: IModalProps<any>) {
return (
<Sheet open={visible} modal={false}>
<SheetTrigger>Open</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Are you absolutely sure?</SheetTitle>
<SheetDescription>
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>
);
}

+ 170
- 113
web/src/pages/agent/debug-content/index.tsx 查看文件

@@ -1,30 +1,27 @@
import { Authorization } from '@/constants/authorization';
import { useSetModalState } from '@/hooks/common-hooks';
import { useSetSelectedRecord } from '@/hooks/logic-hooks';
import { useHandleSubmittable } from '@/hooks/login-hooks';
import api from '@/utils/api';
import { getAuthorization } from '@/utils/authorization-util';
import { UploadOutlined } from '@ant-design/icons';
import { FileUploader } from '@/components/file-uploader';
import { ButtonLoading } from '@/components/ui/button';
import {
Button,
Form,
FormItemProps,
Input,
InputNumber,
Select,
Switch,
Upload,
} from 'antd';
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { RAGFlowSelect } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { useSetModalState } from '@/hooks/common-hooks';
import { useSetSelectedRecord } from '@/hooks/logic-hooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { UploadChangeParam, UploadFile } from 'antd/es/upload';
import { pick } from 'lodash';
import { Link } from 'lucide-react';
import React, { useCallback, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { BeginQueryType } from '../constant';
import { BeginQuery } from '../interface';
import { PopoverForm } from './popover-form';

import styles from './index.less';

interface IProps {
parameters: BeginQuery[];
@@ -34,6 +31,8 @@ interface IProps {
submitButtonDisabled?: boolean;
}

const values = {};

const DebugContent = ({
parameters,
ok,
@@ -42,7 +41,20 @@ const DebugContent = ({
submitButtonDisabled = false,
}: IProps) => {
const { t } = useTranslation();
const [form] = Form.useForm();

const FormSchema = useMemo(() => {
const obj = parameters.reduce((pre, cur, idx) => {
pre[idx] = z.string().optional();
return pre;
}, {});
return z.object(obj);
}, [parameters]);

const form = useForm({
defaultValues: values,
resolver: zodResolver(FormSchema),
});

const {
visible,
hideModal: hidePopover,
@@ -50,7 +62,8 @@ const DebugContent = ({
showModal: showPopover,
} = useSetModalState();
const { setRecord, currentRecord } = useSetSelectedRecord<number>();
const { submittable } = useHandleSubmittable(form);
// const { submittable } = useHandleSubmittable(form);
const submittable = true;
const [isUploading, setIsUploading] = useState(false);

const handleShowPopover = useCallback(
@@ -79,8 +92,8 @@ const DebugContent = ({
);

const renderWidget = useCallback(
(q: BeginQuery, idx: number) => {
const props: FormItemProps & { key: number } = {
(q: BeginQuery, idx: string) => {
const props = {
key: idx,
label: q.name ?? q.key,
name: idx,
@@ -89,80 +102,119 @@ const DebugContent = ({
props.rules = [{ required: true }];
}

const urlList: { url: string; result: string }[] =
form.getFieldValue(idx) || [];
// const urlList: { url: string; result: string }[] =
// form.getFieldValue(idx) || [];

const urlList: { url: string; result: string }[] = [];

const BeginQueryTypeMap = {
[BeginQueryType.Line]: (
<Form.Item {...props}>
<Input></Input>
</Form.Item>
<FormField
control={form.control}
name={props.name}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{props.label}</FormLabel>
<FormControl>
<Input {...field}></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
),
[BeginQueryType.Paragraph]: (
<Form.Item {...props}>
<Input.TextArea rows={1}></Input.TextArea>
</Form.Item>
<FormField
control={form.control}
name={props.name}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{props.label}</FormLabel>
<FormControl>
<Textarea rows={1} {...field}></Textarea>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
),
[BeginQueryType.Options]: (
<Form.Item {...props}>
<Select
allowClear
options={q.options?.map((x) => ({ label: x, value: x })) ?? []}
></Select>
</Form.Item>
<FormField
control={form.control}
name={props.name}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{props.label}</FormLabel>
<FormControl>
<RAGFlowSelect
allowClear
options={
q.options?.map((x) => ({ label: x, value: x })) ?? []
}
{...field}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
),
[BeginQueryType.File]: (
<React.Fragment key={idx}>
<Form.Item label={q.name ?? q.key} required={!q.optional}>
<div className="relative">
<Form.Item
{...props}
valuePropName="fileList"
getValueFromEvent={normFile}
noStyle
>
<Upload
name="file"
action={api.parse}
multiple
headers={{ [Authorization]: getAuthorization() }}
onChange={onChange(q.optional)}
>
<Button icon={<UploadOutlined />}>
{t('common.upload')}
</Button>
</Upload>
</Form.Item>
<Form.Item
{...pick(props, ['key', 'label', 'rules'])}
required={!q.optional}
className={urlList.length > 0 ? 'mb-1' : ''}
noStyle
>
<PopoverForm visible={visible} switchVisible={switchVisible}>
<Button
onClick={handleShowPopover(idx)}
className="absolute left-1/2 top-0"
icon={<Link className="size-3" />}
>
{t('flow.pasteFileLink')}
</Button>
</PopoverForm>
</Form.Item>
</div>
</Form.Item>
<Form.Item name={idx} noStyle {...pick(props, ['rules'])} />
<FormField
control={form.control}
name={'file'}
render={({ field }) => (
<div className="space-y-6">
<FormItem className="w-full">
<FormLabel>{t('assistantAvatar')}</FormLabel>
<FormControl>
<FileUploader
value={field.value}
onValueChange={field.onChange}
maxFileCount={1}
maxSize={4 * 1024 * 1024}
/>
</FormControl>
<FormMessage />
</FormItem>
</div>
)}
/>
</React.Fragment>
),
[BeginQueryType.Integer]: (
<Form.Item {...props}>
<InputNumber></InputNumber>
</Form.Item>
<FormField
control={form.control}
name={props.name}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{props.label}</FormLabel>
<FormControl>
<Input type="number" {...field}></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
),
[BeginQueryType.Boolean]: (
<Form.Item valuePropName={'checked'} {...props}>
<Switch></Switch>
</Form.Item>
<FormField
control={form.control}
name={props.name}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{props.label}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
></Switch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
),
};

@@ -171,11 +223,11 @@ const DebugContent = ({
BeginQueryTypeMap[BeginQueryType.Paragraph]
);
},
[form, handleShowPopover, onChange, switchVisible, t, visible],
[form, t],
);

const onOk = useCallback(async () => {
const values = await form.validateFields();
// const values = await form.validateFields();
const nextValues = Object.entries(values).map(([key, value]) => {
const item = parameters[Number(key)];
let nextValue = value;
@@ -193,44 +245,49 @@ const DebugContent = ({
});

ok(nextValues);
}, [form, ok, parameters]);
}, [ok, parameters]);

const onSubmit = useCallback(
(values: z.infer<typeof FormSchema>) => {
const nextValues = Object.entries(values).map(([key, value]) => {
const item = parameters[Number(key)];
let nextValue = value;
if (Array.isArray(value)) {
nextValue = ``;

value.forEach((x) => {
nextValue +=
x?.originFileObj instanceof File
? `${x.name}\n${x.response?.data}\n----\n`
: `${x.url}\n${x.result}\n----\n`;
});
}
return { ...item, value: nextValue };
});

ok(nextValues);
},
[ok, parameters],
);

return (
<>
<section className={styles.formWrapper}>
<Form.Provider
onFormFinish={(name, { values, forms }) => {
if (name === 'urlForm') {
const { basicForm } = forms;
const urlInfo = basicForm.getFieldValue(currentRecord) || [];
basicForm.setFieldsValue({
[currentRecord]: [...urlInfo, { ...values, name: values.url }],
});
hidePopover();
}
}}
>
<Form
name="basicForm"
autoComplete="off"
layout={'vertical'}
form={form}
>
<section>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
{parameters.map((x, idx) => {
return renderWidget(x, idx);
return <div key={idx}>{renderWidget(x, idx.toString())}</div>;
})}
</Form>
</Form.Provider>
</form>
</Form>
</section>
<Button
type={'primary'}
block
<ButtonLoading
onClick={onOk}
loading={loading}
disabled={!submittable || isUploading || submitButtonDisabled}
>
{t(isNext ? 'common.next' : 'flow.run')}
</Button>
</ButtonLoading>
</>
);
};

+ 68
- 39
web/src/pages/agent/debug-content/popover-form.tsx 查看文件

@@ -1,74 +1,103 @@
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Popover, PopoverContent } from '@/components/ui/popover';
import { useParseDocument } from '@/hooks/document-hooks';
import { useResetFormOnCloseModal } from '@/hooks/logic-hooks';
import { IModalProps } from '@/interfaces/common';
import { Button, Form, Input, Popover } from 'antd';
import { zodResolver } from '@hookform/resolvers/zod';
import { PropsWithChildren } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';

const reg =
/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/;

const FormSchema = z.object({
url: z.string(),
result: z.any(),
});

const values = {
url: '',
result: null,
};

export const PopoverForm = ({
children,
visible,
switchVisible,
}: PropsWithChildren<IModalProps<any>>) => {
const [form] = Form.useForm();
const form = useForm({
defaultValues: values,
resolver: zodResolver(FormSchema),
});
const { parseDocument, loading } = useParseDocument();
const { t } = useTranslation();

useResetFormOnCloseModal({
form,
visible,
});
// useResetFormOnCloseModal({
// form,
// visible,
// });

const onOk = async () => {
const values = await form.validateFields();
async function onSubmit(values: z.infer<typeof FormSchema>) {
const val = values.url;

if (reg.test(val)) {
const ret = await parseDocument(val);
if (ret?.data?.code === 0) {
form.setFieldValue('result', ret?.data?.data);
form.submit();
form.setValue('result', ret?.data?.data);
}
}
};
}

const content = (
<Form form={form} name="urlForm">
<Form.Item
name="url"
rules={[{ required: true, type: 'url' }]}
className="m-0"
>
<Input
onPressEnter={(e) => e.preventDefault()}
placeholder={t('flow.pasteFileLink')}
suffix={
<Button
type="primary"
onClick={onOk}
size={'small'}
loading={loading}
>
{t('common.submit')}
</Button>
}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name={`url`}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Input
{...field}
// onPressEnter={(e) => e.preventDefault()}
placeholder={t('flow.pasteFileLink')}
// suffix={
// <Button
// type="primary"
// onClick={onOk}
// size={'small'}
// loading={loading}
// >
// {t('common.submit')}
// </Button>
// }
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`result`}
render={() => <></>}
/>
</Form.Item>
<Form.Item name={'result'} noStyle />
</form>
</Form>
);

return (
<Popover
content={content}
open={visible}
trigger={'click'}
onOpenChange={switchVisible}
>
<Popover open={visible} onOpenChange={switchVisible}>
{children}
<PopoverContent>{content}</PopoverContent>
</Popover>
);
};

+ 25
- 6
web/src/pages/agent/index.tsx 查看文件

@@ -12,14 +12,19 @@ import { useSetModalState } from '@/hooks/common-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { ReactFlowProvider } from '@xyflow/react';
import { CodeXml, EllipsisVertical, Forward, Import, Key } from 'lucide-react';
import { ComponentPropsWithoutRef } from 'react';
import { ComponentPropsWithoutRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { AgentSidebar } from './agent-sidebar';
import FlowCanvas from './canvas';
import AgentCanvas from './canvas';
import { useHandleExportOrImportJsonFile } from './hooks/use-export-json';
import { useFetchDataOnMount } from './hooks/use-fetch-data';
import { useGetBeginNodeDataQuery } from './hooks/use-get-begin-query';
import { useOpenDocument } from './hooks/use-open-document';
import { useSaveGraph } from './hooks/use-save-graph';
import {
useSaveGraph,
useSaveGraphBeforeOpeningDebugDrawer,
} from './hooks/use-save-graph';
import { BeginQuery } from './interface';
import { UploadAgentDialog } from './upload-agent-dialog';

function AgentDropdownMenuItem({
@@ -52,6 +57,18 @@ export default function Agent() {
const { saveGraph, loading } = useSaveGraph();

const { flowDetail } = useFetchDataOnMount();
const getBeginNodeDataQuery = useGetBeginNodeDataQuery();

const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer);

const handleRunAgent = useCallback(() => {
const query: BeginQuery[] = getBeginNodeDataQuery();
if (query.length > 0) {
showChatDrawer();
} else {
handleRun();
}
}, [getBeginNodeDataQuery, handleRun, showChatDrawer]);

return (
<section>
@@ -64,7 +81,9 @@ export default function Agent() {
>
Save
</ButtonLoading>
<Button variant={'outline'}>Run app</Button>
<Button variant={'outline'} onClick={handleRunAgent}>
Run app
</Button>
<Button variant={'outline'}>Publish</Button>

<DropdownMenu>
@@ -104,10 +123,10 @@ export default function Agent() {
<div className="w-full">
<SidebarTrigger />
<div className="w-full h-full">
<FlowCanvas
<AgentCanvas
drawerVisible={chatDrawerVisible}
hideDrawer={hideChatDrawer}
></FlowCanvas>
></AgentCanvas>
</div>
</div>
</SidebarProvider>

+ 62
- 0
web/src/pages/agent/run-sheet/index.tsx 查看文件

@@ -0,0 +1,62 @@
import { IModalProps } from '@/interfaces/common';
import { Drawer } from 'antd';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { BeginId } from '../constant';
import DebugContent from '../debug-content';
import { useGetBeginNodeDataQuery } from '../hooks/use-get-begin-query';
import { useSaveGraphBeforeOpeningDebugDrawer } from '../hooks/use-save-graph';
import { BeginQuery } from '../interface';
import useGraphStore from '../store';
import { getDrawerWidth } from '../utils';

const RunSheet = ({
hideModal,
showModal: showChatModal,
}: IModalProps<any>) => {
const { t } = useTranslation();
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);

const getBeginNodeDataQuery = useGetBeginNodeDataQuery();
const query: BeginQuery[] = getBeginNodeDataQuery();

const { handleRun, loading } = useSaveGraphBeforeOpeningDebugDrawer(
showChatModal!,
);

const handleRunAgent = useCallback(
(nextValues: Record<string, any>) => {
const currentNodes = updateNodeForm(BeginId, nextValues, ['query']);
handleRun(currentNodes);
hideModal?.();
},
[handleRun, hideModal, updateNodeForm],
);

const onOk = useCallback(
async (nextValues: any[]) => {
handleRunAgent(nextValues);
},
[handleRunAgent],
);

return (
<Drawer
title={t('flow.testRun')}
placement="right"
onClose={hideModal}
open
getContainer={false}
width={getDrawerWidth()}
mask={false}
>
<DebugContent
ok={onOk}
parameters={query}
loading={loading}
></DebugContent>
</Drawer>
);
};

export default RunSheet;

正在加载...
取消
保存