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)
This commit is contained in:
balibabu
2025-06-04 15:56:47 +08:00
committed by GitHub
parent 9938a4cbb6
commit 8445143359
6 changed files with 361 additions and 168 deletions

View File

@ -5,7 +5,7 @@ import {
ReactFlow, ReactFlow,
} from '@xyflow/react'; } from '@xyflow/react';
import '@xyflow/react/dist/style.css'; import '@xyflow/react/dist/style.css';
// import ChatDrawer from '../chat/drawer'; import { ChatSheet } from '../chat/chat-sheet';
import FormSheet from '../form-sheet/next'; import FormSheet from '../form-sheet/next';
import { import {
useHandleDrop, useHandleDrop,
@ -15,7 +15,7 @@ import {
} from '../hooks'; } from '../hooks';
import { useBeforeDelete } from '../hooks/use-before-delete'; import { useBeforeDelete } from '../hooks/use-before-delete';
import { useShowDrawer } from '../hooks/use-show-drawer'; import { useShowDrawer } from '../hooks/use-show-drawer';
// import RunDrawer from '../run-drawer'; import RunSheet from '../run-sheet';
import { ButtonEdge } from './edge'; import { ButtonEdge } from './edge';
import styles from './index.less'; import styles from './index.less';
import { RagNode } from './node'; import { RagNode } from './node';
@ -66,7 +66,7 @@ interface IProps {
hideDrawer(): void; hideDrawer(): void;
} }
function FlowCanvas({ drawerVisible, hideDrawer }: IProps) { function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
const { const {
nodes, nodes,
edges, edges,
@ -165,21 +165,21 @@ function FlowCanvas({ drawerVisible, hideDrawer }: IProps) {
showSingleDebugDrawer={showSingleDebugDrawer} showSingleDebugDrawer={showSingleDebugDrawer}
></FormSheet> ></FormSheet>
)} )}
{/* {chatVisible && ( {chatVisible && (
<ChatDrawer <ChatSheet
visible={chatVisible} visible={chatVisible}
hideModal={hideRunOrChatDrawer} hideModal={hideRunOrChatDrawer}
></ChatDrawer> ></ChatSheet>
)} )}
{runVisible && ( {runVisible && (
<RunDrawer <RunSheet
hideModal={hideRunOrChatDrawer} hideModal={hideRunOrChatDrawer}
showModal={showChatModal} showModal={showChatModal}
></RunDrawer> ></RunSheet>
)} */} )}
</div> </div>
); );
} }
export default FlowCanvas; export default AgentCanvas;

View File

@ -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>
);
}

View File

@ -1,30 +1,27 @@
import { Authorization } from '@/constants/authorization'; import { FileUploader } from '@/components/file-uploader';
import { ButtonLoading } from '@/components/ui/button';
import {
Form,
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 { useSetModalState } from '@/hooks/common-hooks';
import { useSetSelectedRecord } from '@/hooks/logic-hooks'; import { useSetSelectedRecord } from '@/hooks/logic-hooks';
import { useHandleSubmittable } from '@/hooks/login-hooks'; import { zodResolver } from '@hookform/resolvers/zod';
import api from '@/utils/api';
import { getAuthorization } from '@/utils/authorization-util';
import { UploadOutlined } from '@ant-design/icons';
import {
Button,
Form,
FormItemProps,
Input,
InputNumber,
Select,
Switch,
Upload,
} from 'antd';
import { UploadChangeParam, UploadFile } from 'antd/es/upload'; import { UploadChangeParam, UploadFile } from 'antd/es/upload';
import { pick } from 'lodash'; import React, { useCallback, useMemo, useState } from 'react';
import { Link } from 'lucide-react'; import { useForm } from 'react-hook-form';
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { BeginQueryType } from '../constant'; import { BeginQueryType } from '../constant';
import { BeginQuery } from '../interface'; import { BeginQuery } from '../interface';
import { PopoverForm } from './popover-form';
import styles from './index.less';
interface IProps { interface IProps {
parameters: BeginQuery[]; parameters: BeginQuery[];
@ -34,6 +31,8 @@ interface IProps {
submitButtonDisabled?: boolean; submitButtonDisabled?: boolean;
} }
const values = {};
const DebugContent = ({ const DebugContent = ({
parameters, parameters,
ok, ok,
@ -42,7 +41,20 @@ const DebugContent = ({
submitButtonDisabled = false, submitButtonDisabled = false,
}: IProps) => { }: IProps) => {
const { t } = useTranslation(); 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 { const {
visible, visible,
hideModal: hidePopover, hideModal: hidePopover,
@ -50,7 +62,8 @@ const DebugContent = ({
showModal: showPopover, showModal: showPopover,
} = useSetModalState(); } = useSetModalState();
const { setRecord, currentRecord } = useSetSelectedRecord<number>(); const { setRecord, currentRecord } = useSetSelectedRecord<number>();
const { submittable } = useHandleSubmittable(form); // const { submittable } = useHandleSubmittable(form);
const submittable = true;
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const handleShowPopover = useCallback( const handleShowPopover = useCallback(
@ -79,8 +92,8 @@ const DebugContent = ({
); );
const renderWidget = useCallback( const renderWidget = useCallback(
(q: BeginQuery, idx: number) => { (q: BeginQuery, idx: string) => {
const props: FormItemProps & { key: number } = { const props = {
key: idx, key: idx,
label: q.name ?? q.key, label: q.name ?? q.key,
name: idx, name: idx,
@ -89,80 +102,119 @@ const DebugContent = ({
props.rules = [{ required: true }]; props.rules = [{ required: true }];
} }
const urlList: { url: string; result: string }[] = // const urlList: { url: string; result: string }[] =
form.getFieldValue(idx) || []; // form.getFieldValue(idx) || [];
const urlList: { url: string; result: string }[] = [];
const BeginQueryTypeMap = { const BeginQueryTypeMap = {
[BeginQueryType.Line]: ( [BeginQueryType.Line]: (
<Form.Item {...props}> <FormField
<Input></Input> control={form.control}
</Form.Item> name={props.name}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{props.label}</FormLabel>
<FormControl>
<Input {...field}></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
), ),
[BeginQueryType.Paragraph]: ( [BeginQueryType.Paragraph]: (
<Form.Item {...props}> <FormField
<Input.TextArea rows={1}></Input.TextArea> control={form.control}
</Form.Item> name={props.name}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{props.label}</FormLabel>
<FormControl>
<Textarea rows={1} {...field}></Textarea>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
), ),
[BeginQueryType.Options]: ( [BeginQueryType.Options]: (
<Form.Item {...props}> <FormField
<Select control={form.control}
allowClear name={props.name}
options={q.options?.map((x) => ({ label: x, value: x })) ?? []} render={({ field }) => (
></Select> <FormItem className="flex-1">
</Form.Item> <FormLabel>{props.label}</FormLabel>
<FormControl>
<RAGFlowSelect
allowClear
options={
q.options?.map((x) => ({ label: x, value: x })) ?? []
}
{...field}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
), ),
[BeginQueryType.File]: ( [BeginQueryType.File]: (
<React.Fragment key={idx}> <React.Fragment key={idx}>
<Form.Item label={q.name ?? q.key} required={!q.optional}> <FormField
<div className="relative"> control={form.control}
<Form.Item name={'file'}
{...props} render={({ field }) => (
valuePropName="fileList" <div className="space-y-6">
getValueFromEvent={normFile} <FormItem className="w-full">
noStyle <FormLabel>{t('assistantAvatar')}</FormLabel>
> <FormControl>
<Upload <FileUploader
name="file" value={field.value}
action={api.parse} onValueChange={field.onChange}
multiple maxFileCount={1}
headers={{ [Authorization]: getAuthorization() }} maxSize={4 * 1024 * 1024}
onChange={onChange(q.optional)} />
> </FormControl>
<Button icon={<UploadOutlined />}> <FormMessage />
{t('common.upload')} </FormItem>
</Button> </div>
</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'])} />
</React.Fragment> </React.Fragment>
), ),
[BeginQueryType.Integer]: ( [BeginQueryType.Integer]: (
<Form.Item {...props}> <FormField
<InputNumber></InputNumber> control={form.control}
</Form.Item> name={props.name}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{props.label}</FormLabel>
<FormControl>
<Input type="number" {...field}></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
), ),
[BeginQueryType.Boolean]: ( [BeginQueryType.Boolean]: (
<Form.Item valuePropName={'checked'} {...props}> <FormField
<Switch></Switch> control={form.control}
</Form.Item> 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] BeginQueryTypeMap[BeginQueryType.Paragraph]
); );
}, },
[form, handleShowPopover, onChange, switchVisible, t, visible], [form, t],
); );
const onOk = useCallback(async () => { const onOk = useCallback(async () => {
const values = await form.validateFields(); // const values = await form.validateFields();
const nextValues = Object.entries(values).map(([key, value]) => { const nextValues = Object.entries(values).map(([key, value]) => {
const item = parameters[Number(key)]; const item = parameters[Number(key)];
let nextValue = value; let nextValue = value;
@ -193,44 +245,49 @@ const DebugContent = ({
}); });
ok(nextValues); 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 ( return (
<> <>
<section className={styles.formWrapper}> <section>
<Form.Provider <Form {...form}>
onFormFinish={(name, { values, forms }) => { <form onSubmit={form.handleSubmit(onSubmit)}>
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}
>
{parameters.map((x, idx) => { {parameters.map((x, idx) => {
return renderWidget(x, idx); return <div key={idx}>{renderWidget(x, idx.toString())}</div>;
})} })}
</Form> </form>
</Form.Provider> </Form>
</section> </section>
<Button <ButtonLoading
type={'primary'}
block
onClick={onOk} onClick={onOk}
loading={loading} loading={loading}
disabled={!submittable || isUploading || submitButtonDisabled} disabled={!submittable || isUploading || submitButtonDisabled}
> >
{t(isNext ? 'common.next' : 'flow.run')} {t(isNext ? 'common.next' : 'flow.run')}
</Button> </ButtonLoading>
</> </>
); );
}; };

View File

@ -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 { useParseDocument } from '@/hooks/document-hooks';
import { useResetFormOnCloseModal } from '@/hooks/logic-hooks';
import { IModalProps } from '@/interfaces/common'; import { IModalProps } from '@/interfaces/common';
import { Button, Form, Input, Popover } from 'antd'; import { zodResolver } from '@hookform/resolvers/zod';
import { PropsWithChildren } from 'react'; import { PropsWithChildren } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { z } from 'zod';
const reg = const reg =
/^(((ht|f)tps?):\/\/)?([^!@#$%^&*?.\s-]([^!@#$%^&*?.\s]{0,63}[^!@#$%^&*?.\s])?\.)+[a-z]{2,6}\/?/; /^(((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 = ({ export const PopoverForm = ({
children, children,
visible, visible,
switchVisible, switchVisible,
}: PropsWithChildren<IModalProps<any>>) => { }: PropsWithChildren<IModalProps<any>>) => {
const [form] = Form.useForm(); const form = useForm({
defaultValues: values,
resolver: zodResolver(FormSchema),
});
const { parseDocument, loading } = useParseDocument(); const { parseDocument, loading } = useParseDocument();
const { t } = useTranslation(); const { t } = useTranslation();
useResetFormOnCloseModal({ // useResetFormOnCloseModal({
form, // form,
visible, // visible,
}); // });
const onOk = async () => { async function onSubmit(values: z.infer<typeof FormSchema>) {
const values = await form.validateFields();
const val = values.url; const val = values.url;
if (reg.test(val)) { if (reg.test(val)) {
const ret = await parseDocument(val); const ret = await parseDocument(val);
if (ret?.data?.code === 0) { if (ret?.data?.code === 0) {
form.setFieldValue('result', ret?.data?.data); form.setValue('result', ret?.data?.data);
form.submit();
} }
} }
}; }
const content = ( const content = (
<Form form={form} name="urlForm"> <Form {...form}>
<Form.Item <form onSubmit={form.handleSubmit(onSubmit)}>
name="url" <FormField
rules={[{ required: true, type: 'url' }]} control={form.control}
className="m-0" name={`url`}
> render={({ field }) => (
<Input <FormItem className="flex-1">
onPressEnter={(e) => e.preventDefault()} <FormControl>
placeholder={t('flow.pasteFileLink')} <Input
suffix={ {...field}
<Button // onPressEnter={(e) => e.preventDefault()}
type="primary" placeholder={t('flow.pasteFileLink')}
onClick={onOk} // suffix={
size={'small'} // <Button
loading={loading} // type="primary"
> // onClick={onOk}
{t('common.submit')} // size={'small'}
</Button> // loading={loading}
} // >
// {t('common.submit')}
// </Button>
// }
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/> />
</Form.Item> <FormField
<Form.Item name={'result'} noStyle /> control={form.control}
name={`result`}
render={() => <></>}
/>
</form>
</Form> </Form>
); );
return ( return (
<Popover <Popover open={visible} onOpenChange={switchVisible}>
content={content}
open={visible}
trigger={'click'}
onOpenChange={switchVisible}
>
{children} {children}
<PopoverContent>{content}</PopoverContent>
</Popover> </Popover>
); );
}; };

View File

@ -12,14 +12,19 @@ import { useSetModalState } from '@/hooks/common-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { ReactFlowProvider } from '@xyflow/react'; import { ReactFlowProvider } from '@xyflow/react';
import { CodeXml, EllipsisVertical, Forward, Import, Key } from 'lucide-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 { useTranslation } from 'react-i18next';
import { AgentSidebar } from './agent-sidebar'; import { AgentSidebar } from './agent-sidebar';
import FlowCanvas from './canvas'; import AgentCanvas from './canvas';
import { useHandleExportOrImportJsonFile } from './hooks/use-export-json'; import { useHandleExportOrImportJsonFile } from './hooks/use-export-json';
import { useFetchDataOnMount } from './hooks/use-fetch-data'; import { useFetchDataOnMount } from './hooks/use-fetch-data';
import { useGetBeginNodeDataQuery } from './hooks/use-get-begin-query';
import { useOpenDocument } from './hooks/use-open-document'; 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'; import { UploadAgentDialog } from './upload-agent-dialog';
function AgentDropdownMenuItem({ function AgentDropdownMenuItem({
@ -52,6 +57,18 @@ export default function Agent() {
const { saveGraph, loading } = useSaveGraph(); const { saveGraph, loading } = useSaveGraph();
const { flowDetail } = useFetchDataOnMount(); 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 ( return (
<section> <section>
@ -64,7 +81,9 @@ export default function Agent() {
> >
Save Save
</ButtonLoading> </ButtonLoading>
<Button variant={'outline'}>Run app</Button> <Button variant={'outline'} onClick={handleRunAgent}>
Run app
</Button>
<Button variant={'outline'}>Publish</Button> <Button variant={'outline'}>Publish</Button>
<DropdownMenu> <DropdownMenu>
@ -104,10 +123,10 @@ export default function Agent() {
<div className="w-full"> <div className="w-full">
<SidebarTrigger /> <SidebarTrigger />
<div className="w-full h-full"> <div className="w-full h-full">
<FlowCanvas <AgentCanvas
drawerVisible={chatDrawerVisible} drawerVisible={chatDrawerVisible}
hideDrawer={hideChatDrawer} hideDrawer={hideChatDrawer}
></FlowCanvas> ></AgentCanvas>
</div> </div>
</div> </div>
</SidebarProvider> </SidebarProvider>

View File

@ -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;