Feat: Initialize the data pipeline canvas. #9869 (#9870)

### What problem does this PR solve?
Feat: Initialize the data pipeline canvas. #9869

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-09-02 15:47:33 +08:00
committed by GitHub
parent c2567844ea
commit cb14dafaca
196 changed files with 21201 additions and 0 deletions

View File

@ -0,0 +1,191 @@
import { BlockButton } from '@/components/ui/button';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import { Position } from '@xyflow/react';
import { t } from 'i18next';
import { PencilLine, X } from 'lucide-react';
import {
MouseEventHandler,
PropsWithChildren,
useCallback,
useContext,
useMemo,
} from 'react';
import { Operator } from '../../constant';
import { AgentInstanceContext } from '../../context';
import { useFindMcpById } from '../../hooks/use-find-mcp-by-id';
import { INextOperatorForm } from '../../interface';
import OperatorIcon from '../../operator-icon';
import useGraphStore from '../../store';
import { filterDownstreamAgentNodeIds } from '../../utils/filter-downstream-nodes';
import { ToolPopover } from './tool-popover';
import { useDeleteAgentNodeMCP } from './tool-popover/use-update-mcp';
import { useDeleteAgentNodeTools } from './tool-popover/use-update-tools';
import { useGetAgentMCPIds, useGetAgentToolNames } from './use-get-tools';
export function ToolCard({
children,
className,
...props
}: PropsWithChildren & React.HTMLAttributes<HTMLLIElement>) {
const element = useMemo(() => {
return (
<li
{...props}
className={cn(
'flex bg-bg-card p-1 rounded-sm justify-between',
className,
)}
>
{children}
</li>
);
}, [children, className, props]);
if (children === Operator.Code) {
return (
<Tooltip>
<TooltipTrigger asChild>{element}</TooltipTrigger>
<TooltipContent>
<p>It doesn't have any config.</p>
</TooltipContent>
</Tooltip>
);
}
return element;
}
type ActionButtonProps<T> = {
record: T;
deleteRecord(record: T): void;
edit: MouseEventHandler<HTMLOrSVGElement>;
};
function ActionButton<T>({ deleteRecord, record, edit }: ActionButtonProps<T>) {
const handleDelete = useCallback(() => {
deleteRecord(record);
}, [deleteRecord, record]);
return (
<div className="flex items-center gap-2 text-text-secondary">
<PencilLine
className="size-4 cursor-pointer"
data-tool={record}
onClick={edit}
/>
<X className="size-4 cursor-pointer" onClick={handleDelete} />
</div>
);
}
export function AgentTools() {
const { toolNames } = useGetAgentToolNames();
const { deleteNodeTool } = useDeleteAgentNodeTools();
const { mcpIds } = useGetAgentMCPIds();
const { findMcpById } = useFindMcpById();
const { deleteNodeMCP } = useDeleteAgentNodeMCP();
const { showFormDrawer } = useContext(AgentInstanceContext);
const { clickedNodeId, findAgentToolNodeById, selectNodeIds } = useGraphStore(
(state) => state,
);
const handleEdit: MouseEventHandler<SVGSVGElement> = useCallback(
(e) => {
const toolNodeId = findAgentToolNodeById(clickedNodeId);
if (toolNodeId) {
selectNodeIds([toolNodeId]);
showFormDrawer(e, toolNodeId);
}
},
[clickedNodeId, findAgentToolNodeById, selectNodeIds, showFormDrawer],
);
return (
<section className="space-y-2.5">
<span className="text-text-secondary">{t('flow.tools')}</span>
<ul className="space-y-2">
{toolNames.map((x) => (
<ToolCard key={x}>
<div className="flex gap-2 items-center">
<OperatorIcon name={x as Operator}></OperatorIcon>
{x}
</div>
<ActionButton
record={x}
deleteRecord={deleteNodeTool(x)}
edit={handleEdit}
></ActionButton>
</ToolCard>
))}
{mcpIds.map((id) => (
<ToolCard key={id}>
{findMcpById(id)?.name}
<ActionButton
record={id}
deleteRecord={deleteNodeMCP(id)}
edit={handleEdit}
></ActionButton>
</ToolCard>
))}
</ul>
<ToolPopover>
<BlockButton>{t('flow.addTools')}</BlockButton>
</ToolPopover>
</section>
);
}
export function Agents({ node }: INextOperatorForm) {
const { addCanvasNode } = useContext(AgentInstanceContext);
const { deleteAgentDownstreamNodesById, edges, getNode, selectNodeIds } =
useGraphStore((state) => state);
const { showFormDrawer } = useContext(AgentInstanceContext);
const handleEdit = useCallback(
(nodeId: string): MouseEventHandler<SVGSVGElement> =>
(e) => {
selectNodeIds([nodeId]);
showFormDrawer(e, nodeId);
},
[selectNodeIds, showFormDrawer],
);
const subBottomAgentNodeIds = useMemo(() => {
return filterDownstreamAgentNodeIds(edges, node?.id);
}, [edges, node?.id]);
return (
<section className="space-y-2.5">
<span className="text-text-secondary">{t('flow.agent')}</span>
<ul className="space-y-2">
{subBottomAgentNodeIds.map((id) => {
const currentNode = getNode(id);
return (
<ToolCard key={id}>
{currentNode?.data.name}
<ActionButton
record={id}
deleteRecord={deleteAgentDownstreamNodesById}
edit={handleEdit(id)}
></ActionButton>
</ToolCard>
);
})}
</ul>
<BlockButton
onClick={addCanvasNode(Operator.Agent, {
nodeId: node?.id,
position: Position.Bottom,
})}
>
{t('flow.addAgent')}
</BlockButton>
</section>
);
}

View File

@ -0,0 +1,93 @@
import { BlockButton, Button } from '@/components/ui/button';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { RAGFlowSelect } from '@/components/ui/select';
import { X } from 'lucide-react';
import { memo } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { PromptRole } from '../../constant';
import { PromptEditor } from '../components/prompt-editor';
const options = [
{ label: 'User', value: PromptRole.User },
{ label: 'Assistant', value: PromptRole.Assistant },
];
const DynamicPrompt = () => {
const { t } = useTranslation();
const form = useFormContext();
const name = 'prompts';
const { fields, append, remove } = useFieldArray({
name: name,
control: form.control,
});
return (
<FormItem>
<FormLabel tooltip={t('flow.msgTip')}>{t('flow.msg')}</FormLabel>
<div className="space-y-4">
{fields.map((field, index) => (
<div key={field.id} className="flex">
<div className="space-y-2 flex-1">
<FormField
control={form.control}
name={`${name}.${index}.role`}
render={({ field }) => (
<FormItem className="w-1/3">
<FormLabel />
<FormControl>
<RAGFlowSelect
{...field}
options={options}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`${name}.${index}.content`}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<section>
<PromptEditor
{...field}
showToolbar={false}
></PromptEditor>
</section>
</FormControl>
</FormItem>
)}
/>
</div>
<Button
type="button"
variant={'ghost'}
onClick={() => remove(index)}
>
<X />
</Button>
</div>
))}
</div>
<FormMessage />
<BlockButton
onClick={() => append({ content: '', role: PromptRole.User })}
>
Add
</BlockButton>
</FormItem>
);
};
export default memo(DynamicPrompt);

View File

@ -0,0 +1,63 @@
import { BlockButton, Button } from '@/components/ui/button';
import {
FormControl,
FormField,
FormItem,
FormMessage,
} from '@/components/ui/form';
import { X } from 'lucide-react';
import { memo } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { PromptEditor } from '../components/prompt-editor';
const DynamicTool = () => {
const form = useFormContext();
const name = 'tools';
const { fields, append, remove } = useFieldArray({
name: name,
control: form.control,
});
return (
<FormItem>
<div className="space-y-4">
{fields.map((field, index) => (
<div key={field.id} className="flex">
<div className="space-y-2 flex-1">
<FormField
control={form.control}
name={`${name}.${index}.component_name`}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<section>
<PromptEditor
{...field}
showToolbar={false}
></PromptEditor>
</section>
</FormControl>
</FormItem>
)}
/>
</div>
<Button
type="button"
variant={'ghost'}
onClick={() => remove(index)}
>
<X />
</Button>
</div>
))}
</div>
<FormMessage />
<BlockButton onClick={() => append({ component_name: '' })}>
Add
</BlockButton>
</FormItem>
);
};
export default memo(DynamicTool);

View File

@ -0,0 +1,280 @@
import { Collapse } from '@/components/collapse';
import { FormContainer } from '@/components/form-container';
import {
LargeModelFilterFormSchema,
LargeModelFormField,
} from '@/components/large-model-form-field';
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
} from '@/components/ui/form';
import { Input, NumberInput } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { LlmModelType } from '@/constants/knowledge';
import { useFindLlmByUuid } from '@/hooks/use-llm-request';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo, useEffect, useMemo } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import {
AgentExceptionMethod,
NodeHandleId,
VariableType,
initialAgentValues,
} from '../../constant';
import { INextOperatorForm } from '../../interface';
import useGraphStore from '../../store';
import { isBottomSubAgent } from '../../utils';
import { buildOutputList } from '../../utils/build-output-list';
import { DescriptionField } from '../components/description-field';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { PromptEditor } from '../components/prompt-editor';
import { QueryVariable } from '../components/query-variable';
import { AgentTools, Agents } from './agent-tools';
import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-change';
const FormSchema = z.object({
sys_prompt: z.string(),
description: z.string().optional(),
user_prompt: z.string().optional(),
prompts: z.string().optional(),
// prompts: z
// .array(
// z.object({
// role: z.string(),
// content: z.string(),
// }),
// )
// .optional(),
message_history_window_size: z.coerce.number(),
tools: z
.array(
z.object({
component_name: z.string(),
}),
)
.optional(),
...LlmSettingSchema,
max_retries: z.coerce.number(),
delay_after_error: z.coerce.number().optional(),
visual_files_var: z.string().optional(),
max_rounds: z.coerce.number().optional(),
exception_method: z.string().optional(),
exception_goto: z.array(z.string()).optional(),
exception_default_value: z.string().optional(),
...LargeModelFilterFormSchema,
cite: z.boolean().optional(),
});
const outputList = buildOutputList(initialAgentValues.outputs);
function AgentForm({ node }: INextOperatorForm) {
const { t } = useTranslation();
const { edges, deleteEdgesBySourceAndSourceHandle } = useGraphStore(
(state) => state,
);
const defaultValues = useValues(node);
const ExceptionMethodOptions = Object.values(AgentExceptionMethod).map(
(x) => ({
label: t(`flow.${x}`),
value: x,
}),
);
const isSubAgent = useMemo(() => {
return isBottomSubAgent(edges, node?.id);
}, [edges, node?.id]);
const form = useForm<z.infer<typeof FormSchema>>({
defaultValues: defaultValues,
resolver: zodResolver(FormSchema),
});
const llmId = useWatch({ control: form.control, name: 'llm_id' });
const findLlmByUuid = useFindLlmByUuid();
const exceptionMethod = useWatch({
control: form.control,
name: 'exception_method',
});
useEffect(() => {
if (exceptionMethod !== AgentExceptionMethod.Goto) {
if (node?.id) {
deleteEdgesBySourceAndSourceHandle(
node?.id,
NodeHandleId.AgentException,
);
}
}
}, [deleteEdgesBySourceAndSourceHandle, exceptionMethod, node?.id]);
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
{isSubAgent && <DescriptionField></DescriptionField>}
<LargeModelFormField showSpeech2TextModel></LargeModelFormField>
{findLlmByUuid(llmId)?.model_type === LlmModelType.Image2text && (
<QueryVariable
name="visual_files_var"
label="Visual Input File"
type={VariableType.File}
></QueryVariable>
)}
</FormContainer>
<FormContainer>
<FormField
control={form.control}
name={`sys_prompt`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{t('flow.systemPrompt')}</FormLabel>
<FormControl>
<PromptEditor
{...field}
placeholder={t('flow.messagePlaceholder')}
showToolbar={false}
></PromptEditor>
</FormControl>
</FormItem>
)}
/>
</FormContainer>
{isSubAgent || (
<FormContainer>
{/* <DynamicPrompt></DynamicPrompt> */}
<FormField
control={form.control}
name={`prompts`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{t('flow.userPrompt')}</FormLabel>
<FormControl>
<section>
<PromptEditor
{...field}
showToolbar={false}
></PromptEditor>
</section>
</FormControl>
</FormItem>
)}
/>
</FormContainer>
)}
<FormContainer>
<AgentTools></AgentTools>
<Agents node={node}></Agents>
</FormContainer>
<Collapse title={<div>{t('flow.advancedSettings')}</div>}>
<FormContainer>
<MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField>
<FormField
control={form.control}
name={`cite`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel tooltip={t('flow.citeTip')}>
{t('flow.cite')}
</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
></Switch>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name={`max_retries`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{t('flow.maxRetries')}</FormLabel>
<FormControl>
<NumberInput {...field} max={8}></NumberInput>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name={`delay_after_error`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{t('flow.delayEfterError')}</FormLabel>
<FormControl>
<NumberInput {...field} max={5} step={0.1}></NumberInput>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name={`max_rounds`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{t('flow.maxRounds')}</FormLabel>
<FormControl>
<NumberInput {...field}></NumberInput>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name={`exception_method`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{t('flow.exceptionMethod')}</FormLabel>
<FormControl>
<SelectWithSearch
{...field}
options={ExceptionMethodOptions}
allowClear
/>
</FormControl>
</FormItem>
)}
/>
{exceptionMethod === AgentExceptionMethod.Comment && (
<FormField
control={form.control}
name={`exception_default_value`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{t('flow.ExceptionDefaultValue')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
</FormItem>
)}
/>
)}
</FormContainer>
</Collapse>
<Output list={outputList}></Output>
</FormWrapper>
</Form>
);
}
export default memo(AgentForm);

View File

@ -0,0 +1,89 @@
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Operator } from '@/pages/agent/constant';
import { AgentFormContext, AgentInstanceContext } from '@/pages/agent/context';
import useGraphStore from '@/pages/agent/store';
import { Position } from '@xyflow/react';
import { t } from 'i18next';
import { PropsWithChildren, useCallback, useContext, useEffect } from 'react';
import { useGetAgentMCPIds, useGetAgentToolNames } from '../use-get-tools';
import { MCPCommand, ToolCommand } from './tool-command';
import { useUpdateAgentNodeMCP } from './use-update-mcp';
import { useUpdateAgentNodeTools } from './use-update-tools';
enum ToolType {
Common = 'common',
MCP = 'mcp',
}
export function ToolPopover({ children }: PropsWithChildren) {
const { addCanvasNode } = useContext(AgentInstanceContext);
const node = useContext(AgentFormContext);
const { updateNodeTools } = useUpdateAgentNodeTools();
const { toolNames } = useGetAgentToolNames();
const deleteAgentToolNodeById = useGraphStore(
(state) => state.deleteAgentToolNodeById,
);
const { mcpIds } = useGetAgentMCPIds();
const { updateNodeMCP } = useUpdateAgentNodeMCP();
const handleChange = useCallback(
(value: string[]) => {
if (Array.isArray(value) && node?.id) {
updateNodeTools(value);
}
},
[node?.id, updateNodeTools],
);
useEffect(() => {
const total = toolNames.length + mcpIds.length;
if (node?.id) {
if (total > 0) {
addCanvasNode(Operator.Tool, {
position: Position.Bottom,
nodeId: node?.id,
})();
} else {
deleteAgentToolNodeById(node.id);
}
}
}, [
addCanvasNode,
deleteAgentToolNodeById,
mcpIds.length,
node?.id,
toolNames.length,
]);
return (
<Popover>
<PopoverTrigger asChild>{children}</PopoverTrigger>
<PopoverContent className="w-80 p-4">
<Tabs defaultValue={ToolType.Common}>
<TabsList>
<TabsTrigger value={ToolType.Common} className="bg-bg-card">
{t('flow.builtIn')}
</TabsTrigger>
<TabsTrigger value={ToolType.MCP} className="bg-bg-card">
MCP
</TabsTrigger>
</TabsList>
<TabsContent value={ToolType.Common}>
<ToolCommand
onChange={handleChange}
value={toolNames}
></ToolCommand>
</TabsContent>
<TabsContent value={ToolType.MCP}>
<MCPCommand value={mcpIds} onChange={updateNodeMCP}></MCPCommand>
</TabsContent>
</Tabs>
</PopoverContent>
</Popover>
);
}

View File

@ -0,0 +1,178 @@
import { CheckIcon } from 'lucide-react';
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from '@/components/ui/command';
import { useListMcpServer } from '@/hooks/use-mcp-request';
import { cn } from '@/lib/utils';
import { Operator } from '@/pages/agent/constant';
import OperatorIcon from '@/pages/agent/operator-icon';
import { t } from 'i18next';
import { lowerFirst } from 'lodash';
import { PropsWithChildren, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
const Menus = [
{
label: t('flow.search'),
list: [
Operator.TavilySearch,
Operator.TavilyExtract,
Operator.Google,
// Operator.Bing,
Operator.DuckDuckGo,
Operator.Wikipedia,
Operator.SearXNG,
Operator.YahooFinance,
Operator.PubMed,
Operator.GoogleScholar,
Operator.ArXiv,
Operator.WenCai,
],
},
{
label: t('flow.communication'),
list: [Operator.Email],
},
// {
// label: 'Productivity',
// list: [],
// },
{
label: t('flow.developer'),
list: [Operator.GitHub, Operator.ExeSQL, Operator.Code, Operator.Retrieval],
},
];
type ToolCommandProps = {
value?: string[];
onChange?(values: string[]): void;
};
type ToolCommandItemProps = {
toggleOption(id: string): void;
id: string;
isSelected: boolean;
} & ToolCommandProps;
function ToolCommandItem({
toggleOption,
id,
isSelected,
children,
}: ToolCommandItemProps & PropsWithChildren) {
return (
<CommandItem className="cursor-pointer" onSelect={() => toggleOption(id)}>
<div
className={cn(
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
isSelected
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible',
)}
>
<CheckIcon className="h-4 w-4" />
</div>
{children}
</CommandItem>
);
}
function useHandleSelectChange({ onChange, value }: ToolCommandProps) {
const [currentValue, setCurrentValue] = useState<string[]>([]);
const toggleOption = useCallback(
(option: string) => {
const newSelectedValues = currentValue.includes(option)
? currentValue.filter((value) => value !== option)
: [...currentValue, option];
setCurrentValue(newSelectedValues);
onChange?.(newSelectedValues);
},
[currentValue, onChange],
);
useEffect(() => {
if (Array.isArray(value)) {
setCurrentValue(value);
}
}, [value]);
return {
toggleOption,
currentValue,
};
}
export function ToolCommand({ value, onChange }: ToolCommandProps) {
const { t } = useTranslation();
const { toggleOption, currentValue } = useHandleSelectChange({
onChange,
value,
});
return (
<Command>
<CommandInput placeholder={t('flow.typeCommandOrsearch')} />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
{Menus.map((x) => (
<CommandGroup heading={x.label} key={x.label}>
{x.list.map((y) => {
const isSelected = currentValue.includes(y);
return (
<ToolCommandItem
key={y}
id={y}
toggleOption={toggleOption}
isSelected={isSelected}
>
<>
<OperatorIcon name={y as Operator}></OperatorIcon>
<span>{t(`flow.${lowerFirst(y)}`)}</span>
</>
</ToolCommandItem>
);
})}
</CommandGroup>
))}
</CommandList>
</Command>
);
}
export function MCPCommand({ onChange, value }: ToolCommandProps) {
const { data } = useListMcpServer();
const { toggleOption, currentValue } = useHandleSelectChange({
onChange,
value,
});
return (
<Command>
<CommandInput placeholder="Type a command or search..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
{data.mcp_servers.map((item) => {
const isSelected = currentValue.includes(item.id);
return (
<ToolCommandItem
key={item.id}
id={item.id}
isSelected={isSelected}
toggleOption={toggleOption}
>
{item.name}
</ToolCommandItem>
);
})}
</CommandList>
</Command>
);
}

View File

@ -0,0 +1,74 @@
import { useListMcpServer } from '@/hooks/use-mcp-request';
import { IAgentForm } from '@/interfaces/database/agent';
import { AgentFormContext } from '@/pages/agent/context';
import useGraphStore from '@/pages/agent/store';
import { get } from 'lodash';
import { useCallback, useContext, useMemo } from 'react';
export function useGetNodeMCP() {
const node = useContext(AgentFormContext);
return useMemo(() => {
const mcp: IAgentForm['mcp'] = get(node, 'data.form.mcp');
return mcp;
}, [node]);
}
export function useUpdateAgentNodeMCP() {
const { updateNodeForm } = useGraphStore((state) => state);
const node = useContext(AgentFormContext);
const mcpList = useGetNodeMCP();
const { data } = useListMcpServer();
const mcpServers = data.mcp_servers;
const findMcpTools = useCallback(
(mcpId: string) => {
const mcp = mcpServers.find((x) => x.id === mcpId);
return mcp?.variables.tools;
},
[mcpServers],
);
const updateNodeMCP = useCallback(
(value: string[]) => {
if (node?.id) {
const nextValue = value.reduce<IAgentForm['mcp']>((pre, cur) => {
const mcp = mcpList.find((x) => x.mcp_id === cur);
const tools = findMcpTools(cur);
if (mcp) {
pre.push(mcp);
} else if (tools) {
pre.push({
mcp_id: cur,
tools: {},
});
}
return pre;
}, []);
updateNodeForm(node?.id, nextValue, ['mcp']);
}
},
[node?.id, updateNodeForm, mcpList, findMcpTools],
);
return { updateNodeMCP };
}
export function useDeleteAgentNodeMCP() {
const { updateNodeForm } = useGraphStore((state) => state);
const mcpList = useGetNodeMCP();
const node = useContext(AgentFormContext);
const deleteNodeMCP = useCallback(
(value: string) => () => {
const nextMCP = mcpList.filter((x) => x.mcp_id !== value);
if (node?.id) {
updateNodeForm(node?.id, nextMCP, ['mcp']);
}
},
[node?.id, mcpList, updateNodeForm],
);
return { deleteNodeMCP };
}

View File

@ -0,0 +1,66 @@
import { IAgentForm } from '@/interfaces/database/agent';
import { Operator } from '@/pages/agent/constant';
import { AgentFormContext } from '@/pages/agent/context';
import { useAgentToolInitialValues } from '@/pages/agent/hooks/use-agent-tool-initial-values';
import useGraphStore from '@/pages/agent/store';
import { get } from 'lodash';
import { useCallback, useContext, useMemo } from 'react';
export function useGetNodeTools() {
const node = useContext(AgentFormContext);
return useMemo(() => {
const tools: IAgentForm['tools'] = get(node, 'data.form.tools');
return tools;
}, [node]);
}
export function useUpdateAgentNodeTools() {
const { updateNodeForm } = useGraphStore((state) => state);
const node = useContext(AgentFormContext);
const tools = useGetNodeTools();
const { initializeAgentToolValues } = useAgentToolInitialValues();
const updateNodeTools = useCallback(
(value: string[]) => {
if (node?.id) {
const nextValue = value.reduce<IAgentForm['tools']>((pre, cur) => {
const tool = tools.find((x) => x.component_name === cur);
pre.push(
tool
? tool
: {
component_name: cur,
name: cur,
params: initializeAgentToolValues(cur as Operator),
},
);
return pre;
}, []);
updateNodeForm(node?.id, nextValue, ['tools']);
}
},
[initializeAgentToolValues, node?.id, tools, updateNodeForm],
);
return { updateNodeTools };
}
export function useDeleteAgentNodeTools() {
const { updateNodeForm } = useGraphStore((state) => state);
const tools = useGetNodeTools();
const node = useContext(AgentFormContext);
const deleteNodeTool = useCallback(
(value: string) => () => {
const nextTools = tools.filter((x) => x.component_name !== value);
if (node?.id) {
updateNodeForm(node?.id, nextTools, ['tools']);
}
},
[node?.id, tools, updateNodeForm],
);
return { deleteNodeTool };
}

View File

@ -0,0 +1,26 @@
import { IAgentForm } from '@/interfaces/database/agent';
import { get } from 'lodash';
import { useContext, useMemo } from 'react';
import { AgentFormContext } from '../../context';
export function useGetAgentToolNames() {
const node = useContext(AgentFormContext);
const toolNames = useMemo(() => {
const tools: IAgentForm['tools'] = get(node, 'data.form.tools', []);
return tools.map((x) => x.component_name);
}, [node]);
return { toolNames };
}
export function useGetAgentMCPIds() {
const node = useContext(AgentFormContext);
const mcpIds = useMemo(() => {
const ids: IAgentForm['mcp'] = get(node, 'data.form.mcp', []);
return ids.map((x) => x.mcp_id);
}, [node]);
return { mcpIds };
}

View File

@ -0,0 +1,33 @@
import { useFetchModelId } from '@/hooks/logic-hooks';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { get, isEmpty } from 'lodash';
import { useMemo } from 'react';
import { initialAgentValues } from '../../constant';
export function useValues(node?: RAGFlowNodeType) {
const llmId = useFetchModelId();
const defaultValues = useMemo(
() => ({
...initialAgentValues,
llm_id: llmId,
prompts: '',
}),
[llmId],
);
const values = useMemo(() => {
const formData = node?.data?.form;
if (isEmpty(formData)) {
return defaultValues;
}
return {
...formData,
prompts: get(formData, 'prompts.0.content', ''),
};
}, [defaultValues, node?.data?.form]);
return values;
}

View File

@ -0,0 +1,22 @@
import { useEffect } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import { PromptRole } from '../../constant';
import useGraphStore from '../../store';
export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) {
let values = useWatch({ control: form?.control });
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
useEffect(() => {
// Manually triggered form updates are synchronized to the canvas
if (id && form?.formState.isDirty) {
values = form?.getValues();
let nextValues: any = {
...values,
prompts: [{ role: PromptRole.User, content: values.prompts }],
};
updateNodeForm(id, nextValues);
}
}, [form?.formState.isDirty, id, updateNodeForm, values]);
}

View File

@ -0,0 +1,22 @@
import { TopNFormField } from '@/components/top-n-item';
import { Form } from '@/components/ui/form';
import { INextOperatorForm } from '../../interface';
import { DynamicInputVariable } from '../components/next-dynamic-input-variable';
const AkShareForm = ({ form, node }: INextOperatorForm) => {
return (
<Form {...form}>
<form
className="space-y-6"
onSubmit={(e) => {
e.preventDefault();
}}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNFormField max={99}></TopNFormField>
</form>
</Form>
);
};
export default AkShareForm;

View File

@ -0,0 +1,96 @@
import { FormContainer } from '@/components/form-container';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { TopNFormField } from '@/components/top-n-item';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { useTranslate } from '@/hooks/common-hooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo, useMemo } from 'react';
import { useForm, useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { initialArXivValues } from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { QueryVariable } from '../components/query-variable';
export const ArXivFormPartialSchema = {
top_n: z.number(),
sort_by: z.string(),
};
export const FormSchema = z.object({
...ArXivFormPartialSchema,
query: z.string(),
});
export function ArXivFormWidgets() {
const form = useFormContext();
const { t } = useTranslate('flow');
const options = useMemo(() => {
return ['submittedDate', 'lastUpdatedDate', 'relevance'].map((x) => ({
value: x,
label: t(x),
}));
}, [t]);
return (
<>
<TopNFormField></TopNFormField>
<FormField
control={form.control}
name={`sort_by`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{t('sortBy')}</FormLabel>
<FormControl>
<SelectWithSearch {...field} options={options}></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
);
}
const outputList = buildOutputList(initialArXivValues.outputs);
function ArXivForm({ node }: INextOperatorForm) {
const defaultValues = useFormValues(initialArXivValues, node);
const form = useForm<z.infer<typeof FormSchema>>({
defaultValues,
resolver: zodResolver(FormSchema),
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<QueryVariable></QueryVariable>
</FormContainer>
<FormContainer>
<ArXivFormWidgets></ArXivFormWidgets>
</FormContainer>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
}
export default memo(ArXivForm);

View File

@ -0,0 +1,71 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Input, Select } from 'antd';
import { useMemo } from 'react';
import { IOperatorForm } from '../../interface';
import {
BaiduFanyiDomainOptions,
BaiduFanyiSourceLangOptions,
} from '../../options';
import DynamicInputVariable from '../components/dynamic-input-variable';
const BaiduFanyiForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const options = useMemo(() => {
return ['translate', 'fieldtranslate'].map((x) => ({
value: x,
label: t(`baiduSecretKeyOptions.${x}`),
}));
}, [t]);
const baiduFanyiOptions = useMemo(() => {
return BaiduFanyiDomainOptions.map((x) => ({
value: x,
label: t(`baiduDomainOptions.${x}`),
}));
}, [t]);
const baiduFanyiSourceLangOptions = useMemo(() => {
return BaiduFanyiSourceLangOptions.map((x) => ({
value: x,
label: t(`baiduSourceLangOptions.${x}`),
}));
}, [t]);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item label={t('appid')} name={'appid'}>
<Input></Input>
</Form.Item>
<Form.Item label={t('secretKey')} name={'secret_key'}>
<Input></Input>
</Form.Item>
<Form.Item label={t('transType')} name={'trans_type'}>
<Select options={options}></Select>
</Form.Item>
<Form.Item noStyle dependencies={['model_type']}>
{({ getFieldValue }) =>
getFieldValue('trans_type') === 'fieldtranslate' && (
<Form.Item label={t('domain')} name={'domain'}>
<Select options={baiduFanyiOptions}></Select>
</Form.Item>
)
}
</Form.Item>
<Form.Item label={t('sourceLang')} name={'source_lang'}>
<Select options={baiduFanyiSourceLangOptions}></Select>
</Form.Item>
<Form.Item label={t('targetLang')} name={'target_lang'}>
<Select options={baiduFanyiSourceLangOptions}></Select>
</Form.Item>
</Form>
);
};
export default BaiduFanyiForm;

View File

@ -0,0 +1,22 @@
import { TopNFormField } from '@/components/top-n-item';
import { Form } from '@/components/ui/form';
import { INextOperatorForm } from '../../interface';
import { DynamicInputVariable } from '../components/next-dynamic-input-variable';
const BaiduForm = ({ form, node }: INextOperatorForm) => {
return (
<Form {...form}>
<form
className="space-y-6"
onSubmit={(e) => {
e.preventDefault();
}}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNFormField></TopNFormField>
</form>
</Form>
);
};
export default BaiduForm;

View File

@ -0,0 +1,57 @@
'use client';
import { BlockButton, Button } from '@/components/ui/button';
import {
FormControl,
FormField,
FormItem,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { X } from 'lucide-react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
export function BeginDynamicOptions() {
const { t } = useTranslation();
const form = useFormContext();
const name = 'options';
const { fields, remove, append } = useFieldArray({
name: name,
control: form.control,
});
return (
<div className="space-y-5">
{fields.map((field, index) => {
const typeField = `${name}.${index}.value`;
return (
<div key={field.id} className="flex items-center gap-2">
<FormField
control={form.control}
name={typeField}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Input
{...field}
placeholder={t('common.pleaseInput')}
></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button variant={'ghost'} onClick={() => remove(index)}>
<X className="text-text-sub-title-invert " />
</Button>
</div>
);
})}
<BlockButton onClick={() => append({ value: '' })} type="button">
{t('flow.addField')}
</BlockButton>
</div>
);
}

View File

@ -0,0 +1,205 @@
import { Collapse } from '@/components/collapse';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
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 { zodResolver } from '@hookform/resolvers/zod';
import { t } from 'i18next';
import { Plus } from 'lucide-react';
import { memo, useEffect, useRef } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { AgentDialogueMode } from '../../constant';
import { INextOperatorForm } from '../../interface';
import { ParameterDialog } from './parameter-dialog';
import { QueryTable } from './query-table';
import { useEditQueryRecord } from './use-edit-query';
import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-change';
const ModeOptions = [
{ value: AgentDialogueMode.Conversational, label: t('flow.conversational') },
{ value: AgentDialogueMode.Task, label: t('flow.task') },
];
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),
});
useWatchFormChange(node?.id, form);
const inputs = useWatch({ control: form.control, name: 'inputs' });
const mode = useWatch({ control: form.control, name: 'mode' });
const enablePrologue = useWatch({
control: form.control,
name: 'enablePrologue',
});
const previousModeRef = useRef(mode);
useEffect(() => {
if (
previousModeRef.current === AgentDialogueMode.Task &&
mode === AgentDialogueMode.Conversational
) {
form.setValue('enablePrologue', true);
}
previousModeRef.current = mode;
}, [mode, form]);
const {
ok,
currentRecord,
visible,
hideModal,
showModal,
otherThanCurrentQuery,
handleDeleteRecord,
} = useEditQueryRecord({
form,
node,
});
return (
<section className="px-5 space-y-5">
<Form {...form}>
<FormField
control={form.control}
name={'mode'}
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('flow.modeTip')}>
{t('flow.mode')}
</FormLabel>
<FormControl>
<RAGFlowSelect
placeholder={t('common.pleaseSelect')}
options={ModeOptions}
{...field}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{mode === AgentDialogueMode.Conversational && (
<FormField
control={form.control}
name={'enablePrologue'}
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('flow.openingSwitchTip')}>
{t('flow.openingSwitch')}
</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{mode === AgentDialogueMode.Conversational && enablePrologue && (
<FormField
control={form.control}
name={'prologue'}
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('chat.setAnOpenerTip')}>
{t('flow.openingCopy')}
</FormLabel>
<FormControl>
<Textarea
rows={5}
{...field}
placeholder={t('common.pleaseInput')}
></Textarea>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{/* 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>
}
>
<QueryTable
data={inputs}
showModal={showModal}
deleteRecord={handleDeleteRecord}
></QueryTable>
</Collapse>
{visible && (
<ParameterDialog
hideModal={hideModal}
initialValue={currentRecord}
otherThanCurrentQuery={otherThanCurrentQuery}
submit={ok}
></ParameterDialog>
)}
</Form>
</section>
);
}
export default memo(BeginForm);

View File

@ -0,0 +1,226 @@
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { RAGFlowSelect, RAGFlowSelectOptionType } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { zodResolver } from '@hookform/resolvers/zod';
import { isEmpty } from 'lodash';
import { ChangeEvent, useEffect, useMemo } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { BeginQueryType, BeginQueryTypeIconMap } from '../../constant';
import { BeginQuery } from '../../interface';
import { BeginDynamicOptions } from './begin-dynamic-options';
type ModalFormProps = {
initialValue: BeginQuery;
otherThanCurrentQuery: BeginQuery[];
submit(values: any): void;
};
const FormId = 'BeginParameterForm';
function ParameterForm({
initialValue,
otherThanCurrentQuery,
submit,
}: ModalFormProps) {
const { t } = useTranslate('flow');
const FormSchema = z.object({
type: z.string(),
key: z
.string()
.trim()
.min(1)
.refine(
(value) =>
!value || !otherThanCurrentQuery.some((x) => x.key === value),
{ message: 'The key cannot be repeated!' },
),
optional: z.boolean(),
name: z.string().trim().min(1),
options: z
.array(z.object({ value: z.string().or(z.boolean()).or(z.number()) }))
.optional(),
});
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
mode: 'onChange',
defaultValues: {
type: BeginQueryType.Line,
optional: false,
key: '',
name: '',
options: [],
},
});
const options = useMemo(() => {
return Object.values(BeginQueryType).reduce<RAGFlowSelectOptionType[]>(
(pre, cur) => {
const Icon = BeginQueryTypeIconMap[cur];
return [
...pre,
{
label: (
<div className="flex items-center gap-2">
<Icon
className={`size-${cur === BeginQueryType.Options ? 4 : 5}`}
></Icon>
{t(cur.toLowerCase())}
</div>
),
value: cur,
},
];
},
[],
);
}, []);
const type = useWatch({
control: form.control,
name: 'type',
});
useEffect(() => {
if (!isEmpty(initialValue)) {
form.reset({
...initialValue,
options: initialValue.options?.map((x) => ({ value: x })),
});
}
}, [form, initialValue]);
function onSubmit(data: z.infer<typeof FormSchema>) {
const values = { ...data, options: data.options?.map((x) => x.value) };
console.log('🚀 ~ onSubmit ~ values:', values);
submit(values);
}
const handleKeyChange = (e: ChangeEvent<HTMLInputElement>) => {
const name = form.getValues().name || '';
form.setValue('key', e.target.value.trim());
if (!name) {
form.setValue('name', e.target.value.trim());
}
};
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id={FormId}
className="space-y-5"
autoComplete="off"
>
<FormField
name="type"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>{t('type')}</FormLabel>
<FormControl>
<RAGFlowSelect {...field} options={options} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="key"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>{t('key')}</FormLabel>
<FormControl>
<Input {...field} autoComplete="off" onBlur={handleKeyChange} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="name"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>{t('name')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="optional"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>{t('optional')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{type === BeginQueryType.Options && (
<BeginDynamicOptions></BeginDynamicOptions>
)}
</form>
</Form>
);
}
export function ParameterDialog({
initialValue,
hideModal,
otherThanCurrentQuery,
submit,
}: ModalFormProps & IModalProps<BeginQuery>) {
const { t } = useTranslation();
return (
<Dialog open onOpenChange={hideModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('flow.variableSettings')}</DialogTitle>
</DialogHeader>
<ParameterForm
initialValue={initialValue}
otherThanCurrentQuery={otherThanCurrentQuery}
submit={submit}
></ParameterForm>
<DialogFooter>
<Button type="submit" form={FormId}>
{t('modal.okText')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,199 @@
'use client';
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import { Pencil, Trash2 } from 'lucide-react';
import * as React from 'react';
import { TableEmpty } from '@/components/table-skeleton';
import { Button } from '@/components/ui/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
import { BeginQuery } from '../../interface';
interface IProps {
data: BeginQuery[];
deleteRecord(index: number): void;
showModal(index: number, record: BeginQuery): void;
}
export function QueryTable({ data = [], deleteRecord, showModal }: IProps) {
const { t } = useTranslation();
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[],
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const columns: ColumnDef<BeginQuery>[] = [
{
accessorKey: 'key',
header: t('flow.key'),
meta: { cellClassName: 'max-w-30' },
cell: ({ row }) => {
const key: string = row.getValue('key');
return (
<Tooltip>
<TooltipTrigger asChild>
<div className="truncate ">{key}</div>
</TooltipTrigger>
<TooltipContent>
<p>{key}</p>
</TooltipContent>
</Tooltip>
);
},
},
{
accessorKey: 'name',
header: t('flow.name'),
meta: { cellClassName: 'max-w-30' },
cell: ({ row }) => {
const name: string = row.getValue('name');
return (
<Tooltip>
<TooltipTrigger asChild>
<div className="truncate">{name}</div>
</TooltipTrigger>
<TooltipContent>
<p>{name}</p>
</TooltipContent>
</Tooltip>
);
},
},
{
accessorKey: 'type',
header: t('flow.type'),
cell: ({ row }) => (
<div>
{t(`flow.${(row.getValue('type')?.toString() || '').toLowerCase()}`)}
</div>
),
},
{
accessorKey: 'optional',
header: t('flow.optional'),
cell: ({ row }) => <div>{row.getValue('optional') ? 'Yes' : 'No'}</div>,
},
{
id: 'actions',
enableHiding: false,
header: t('common.action'),
cell: ({ row }) => {
const record = row.original;
const idx = row.index;
return (
<div>
<Button
className="bg-transparent text-foreground hover:bg-muted-foreground hover:text-foreground"
onClick={() => showModal(idx, record)}
>
<Pencil />
</Button>
<Button
className="bg-transparent text-foreground hover:bg-muted-foreground hover:text-foreground"
onClick={() => deleteRecord(idx)}
>
<Trash2 />
</Button>
</div>
);
},
},
];
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
state: {
sorting,
columnFilters,
columnVisibility,
},
});
return (
<div className="w-full">
<div className="rounded-md border">
<Table rootClassName="rounded-md">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={cn(cell.column.columnDef.meta?.cellClassName)}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableEmpty columnsLength={columns.length}></TableEmpty>
)}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@ -0,0 +1,67 @@
import { useSetModalState } from '@/hooks/common-hooks';
import { useSetSelectedRecord } from '@/hooks/logic-hooks';
import { useCallback, useMemo, useState } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import { BeginQuery, INextOperatorForm } from '../../interface';
export const useEditQueryRecord = ({
form,
}: INextOperatorForm & { form: UseFormReturn }) => {
const { setRecord, currentRecord } = useSetSelectedRecord<BeginQuery>();
const { visible, hideModal, showModal } = useSetModalState();
const [index, setIndex] = useState(-1);
const inputs: BeginQuery[] = useWatch({
control: form.control,
name: 'inputs',
});
const otherThanCurrentQuery = useMemo(() => {
return inputs.filter((item, idx) => idx !== index);
}, [index, inputs]);
const handleEditRecord = useCallback(
(record: BeginQuery) => {
const inputs: BeginQuery[] = form?.getValues('inputs') || [];
const nextQuery: BeginQuery[] =
index > -1 ? inputs.toSpliced(index, 1, record) : [...inputs, record];
form.setValue('inputs', nextQuery);
hideModal();
},
[form, hideModal, index],
);
const handleShowModal = useCallback(
(idx?: number, record?: BeginQuery) => {
setIndex(idx ?? -1);
setRecord(record ?? ({} as BeginQuery));
showModal();
},
[setRecord, showModal],
);
const handleDeleteRecord = useCallback(
(idx: number) => {
const inputs = form?.getValues('inputs') || [];
const nextInputs = inputs.filter(
(item: BeginQuery, index: number) => index !== idx,
);
form.setValue('inputs', nextInputs);
},
[form],
);
return {
ok: handleEditRecord,
currentRecord,
setRecord,
visible,
hideModal,
showModal: handleShowModal,
otherThanCurrentQuery,
handleDeleteRecord,
};
};

View File

@ -0,0 +1,34 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { AgentDialogueMode } from '../../constant';
import { buildBeginInputListFromObject } from './utils';
export function useValues(node?: RAGFlowNodeType) {
const { t } = useTranslation();
const defaultValues = useMemo(
() => ({
enablePrologue: true,
prologue: t('chat.setAnOpenerInitial'),
mode: AgentDialogueMode.Conversational,
inputs: [],
}),
[t],
);
const values = useMemo(() => {
const formData = node?.data?.form;
if (isEmpty(formData)) {
return defaultValues;
}
const inputs = buildBeginInputListFromObject(formData?.inputs);
return { ...(formData || {}), inputs };
}, [defaultValues, node?.data?.form]);
return values;
}

View File

@ -0,0 +1,31 @@
import { omit } from 'lodash';
import { useEffect } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import { BeginQuery } from '../../interface';
import useGraphStore from '../../store';
export function transferInputsArrayToObject(inputs: BeginQuery[] = []) {
return inputs.reduce<Record<string, Omit<BeginQuery, 'key'>>>((pre, cur) => {
pre[cur.key] = omit(cur, 'key');
return pre;
}, {});
}
export function useWatchFormChange(id?: string, form?: UseFormReturn) {
let values = useWatch({ control: form?.control });
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
useEffect(() => {
if (id) {
values = form?.getValues() || {};
const nextValues = {
...values,
inputs: transferInputsArrayToObject(values.inputs),
};
updateNodeForm(id, nextValues);
}
}, [form?.formState.isDirty, id, updateNodeForm, values]);
}

View File

@ -0,0 +1,14 @@
import { BeginQuery } from '../../interface';
export function buildBeginInputListFromObject(
inputs: Record<string, Omit<BeginQuery, 'key'>>,
) {
return Object.entries(inputs || {}).reduce<BeginQuery[]>(
(pre, [key, value]) => {
pre.push({ ...(value || {}), key });
return pre;
},
[],
);
}

View File

@ -0,0 +1,131 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { TopNFormField } from '@/components/top-n-item';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { useTranslate } from '@/hooks/common-hooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo, useMemo } from 'react';
import { useForm, useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { initialBingValues } from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { BingCountryOptions, BingLanguageOptions } from '../../options';
import { FormWrapper } from '../components/form-wrapper';
import { QueryVariable } from '../components/query-variable';
export const BingFormSchema = {
channel: z.string(),
api_key: z.string(),
country: z.string(),
language: z.string(),
top_n: z.number(),
};
export const FormSchema = z.object({
query: z.string().optional(),
...BingFormSchema,
});
export function BingFormWidgets() {
const form = useFormContext();
const { t } = useTranslate('flow');
const options = useMemo(() => {
return ['Webpages', 'News'].map((x) => ({ label: x, value: x }));
}, []);
return (
<>
<TopNFormField></TopNFormField>
<FormField
control={form.control}
name="channel"
render={({ field }) => (
<FormItem>
<FormLabel>{t('channel')}</FormLabel>
<FormControl>
<SelectWithSearch {...field} options={options}></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="api_key"
render={({ field }) => (
<FormItem>
<FormLabel>{t('apiKey')}</FormLabel>
<FormControl>
<Input {...field}></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="country"
render={({ field }) => (
<FormItem>
<FormLabel>{t('country')}</FormLabel>
<FormControl>
<SelectWithSearch
{...field}
options={BingCountryOptions}
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="language"
render={({ field }) => (
<FormItem>
<FormLabel>{t('language')}</FormLabel>
<FormControl>
<SelectWithSearch
{...field}
options={BingLanguageOptions}
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
);
}
function BingForm({ node }: INextOperatorForm) {
const defaultValues = useFormValues(initialBingValues, node);
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues,
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<QueryVariable></QueryVariable>
<BingFormWidgets></BingFormWidgets>
</FormWrapper>
</Form>
);
}
export default memo(BingForm);

View File

@ -0,0 +1,249 @@
import { Button } from '@/components/ui/button';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { BlurTextarea } from '@/components/ui/textarea';
import { useTranslate } from '@/hooks/common-hooks';
import { PlusOutlined } from '@ant-design/icons';
import { useUpdateNodeInternals } from '@xyflow/react';
import humanId from 'human-id';
import trim from 'lodash/trim';
import { ChevronsUpDown, X } from 'lucide-react';
import {
ChangeEventHandler,
FocusEventHandler,
memo,
useCallback,
useEffect,
useState,
} from 'react';
import { UseFormReturn, useFieldArray, useFormContext } from 'react-hook-form';
import { v4 as uuid } from 'uuid';
import { z } from 'zod';
import useGraphStore from '../../store';
import DynamicExample from './dynamic-example';
import { useCreateCategorizeFormSchema } from './use-form-schema';
interface IProps {
nodeId?: string;
}
interface INameInputProps {
value?: string;
onChange?: (value: string) => void;
otherNames?: string[];
validate(error?: string): void;
}
const getOtherFieldValues = (
form: UseFormReturn,
formListName: string = 'items',
index: number,
latestField: string,
) =>
(form.getValues(formListName) ?? [])
.map((x: any) => x[latestField])
.filter(
(x: string) =>
x !== form.getValues(`${formListName}.${index}.${latestField}`),
);
const InnerNameInput = ({
value,
onChange,
otherNames,
validate,
}: INameInputProps) => {
const [name, setName] = useState<string | undefined>();
const { t } = useTranslate('flow');
const handleNameChange: ChangeEventHandler<HTMLInputElement> = useCallback(
(e) => {
const val = e.target.value;
setName(val);
const trimmedVal = trim(val);
// trigger validation
if (otherNames?.some((x) => x === trimmedVal)) {
validate(t('nameRepeatedMsg'));
} else if (trimmedVal === '') {
validate(t('nameRequiredMsg'));
} else {
validate('');
}
},
[otherNames, validate, t],
);
const handleNameBlur: FocusEventHandler<HTMLInputElement> = useCallback(
(e) => {
const val = e.target.value;
if (otherNames?.every((x) => x !== val) && trim(val) !== '') {
onChange?.(val);
}
},
[onChange, otherNames],
);
useEffect(() => {
setName(value);
}, [value]);
return (
<Input
value={name}
onChange={handleNameChange}
onBlur={handleNameBlur}
></Input>
);
};
const NameInput = memo(InnerNameInput);
const InnerFormSet = ({ index }: IProps & { index: number }) => {
const form = useFormContext();
const { t } = useTranslate('flow');
const buildFieldName = useCallback(
(name: string) => {
return `items.${index}.${name}`;
},
[index],
);
return (
<section className="space-y-4">
<FormField
control={form.control}
name={buildFieldName('name')}
render={({ field }) => (
<FormItem>
<FormLabel>{t('categoryName')}</FormLabel>
<FormControl>
<NameInput
{...field}
otherNames={getOtherFieldValues(form, 'items', index, 'name')}
validate={(error?: string) => {
const fieldName = buildFieldName('name');
if (error) {
form.setError(fieldName, { message: error });
} else {
form.clearErrors(fieldName);
}
}}
></NameInput>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={buildFieldName('description')}
render={({ field }) => (
<FormItem>
<FormLabel>{t('description')}</FormLabel>
<FormControl>
<BlurTextarea {...field} rows={3} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Create a hidden field to make Form instance record this */}
<FormField
control={form.control}
name={'uuid'}
render={() => <div></div>}
/>
<DynamicExample name={buildFieldName('examples')}></DynamicExample>
</section>
);
};
const FormSet = memo(InnerFormSet);
const DynamicCategorize = ({ nodeId }: IProps) => {
const updateNodeInternals = useUpdateNodeInternals();
const FormSchema = useCreateCategorizeFormSchema();
const deleteCategorizeCaseEdges = useGraphStore(
(state) => state.deleteEdgesBySourceAndSourceHandle,
);
const form = useFormContext<z.infer<typeof FormSchema>>();
const { t } = useTranslate('flow');
const { fields, remove, append } = useFieldArray({
name: 'items',
control: form.control,
});
const handleAdd = useCallback(() => {
append({
name: humanId(),
description: '',
uuid: uuid(),
examples: [{ value: '' }],
});
if (nodeId) updateNodeInternals(nodeId);
}, [append, nodeId, updateNodeInternals]);
const handleRemove = useCallback(
(index: number) => () => {
remove(index);
if (nodeId) {
const uuid = fields[index].uuid;
deleteCategorizeCaseEdges(nodeId, uuid);
}
},
[deleteCategorizeCaseEdges, fields, nodeId, remove],
);
return (
<div className="flex flex-col gap-4 ">
{fields.map((field, index) => (
<Collapsible key={field.id} defaultOpen>
<div className="flex items-center justify-between space-x-4">
<h4 className="font-bold">
{form.getValues(`items.${index}.name`)}
</h4>
<CollapsibleTrigger asChild>
<div className="flex gap-4">
<Button
variant="ghost"
size="sm"
className="w-9 p-0"
onClick={handleRemove(index)}
>
<X className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm" className="w-9 p-0">
<ChevronsUpDown className="h-4 w-4" />
<span className="sr-only">Toggle</span>
</Button>
</div>
</CollapsibleTrigger>
</div>
<CollapsibleContent>
<FormSet nodeId={nodeId} index={index}></FormSet>
</CollapsibleContent>
</Collapsible>
))}
<Button type={'button'} onClick={handleAdd}>
<PlusOutlined />
{t('addCategory')}
</Button>
</div>
);
};
export default memo(DynamicCategorize);

View File

@ -0,0 +1,68 @@
import { Button } from '@/components/ui/button';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Textarea } from '@/components/ui/textarea';
import { Plus, X } from 'lucide-react';
import { memo } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
type DynamicExampleProps = { name: string };
const DynamicExample = ({ name }: DynamicExampleProps) => {
const { t } = useTranslation();
const form = useFormContext();
const { fields, append, remove } = useFieldArray({
name: name,
control: form.control,
});
return (
<FormItem>
<FormLabel tooltip={t('flow.msgTip')}>{t('flow.examples')}</FormLabel>
<div className="space-y-4">
{fields.map((field, index) => (
<div key={field.id} className="flex items-start gap-2">
<FormField
control={form.control}
name={`${name}.${index}.value`}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Textarea {...field}> </Textarea>
</FormControl>
</FormItem>
)}
/>
{index === 0 ? (
<Button
type="button"
variant={'ghost'}
onClick={() => append({ value: '' })}
>
<Plus />
</Button>
) : (
<Button
type="button"
variant={'ghost'}
onClick={() => remove(index)}
>
<X />
</Button>
)}
</div>
))}
</div>
<FormMessage />
</FormItem>
);
};
export default memo(DynamicExample);

View File

@ -0,0 +1,48 @@
import { FormContainer } from '@/components/form-container';
import { LargeModelFormField } from '@/components/large-model-form-field';
import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item';
import { Form } from '@/components/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo } from 'react';
import { useForm } from 'react-hook-form';
import { initialCategorizeValues } from '../../constant';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { QueryVariable } from '../components/query-variable';
import DynamicCategorize from './dynamic-categorize';
import { useCreateCategorizeFormSchema } from './use-form-schema';
import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-change';
const outputList = buildOutputList(initialCategorizeValues.outputs);
function CategorizeForm({ node }: INextOperatorForm) {
const values = useValues(node);
const FormSchema = useCreateCategorizeFormSchema();
const form = useForm({
defaultValues: values,
resolver: zodResolver(FormSchema),
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<QueryVariable></QueryVariable>
<LargeModelFormField></LargeModelFormField>
</FormContainer>
<MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField>
<DynamicCategorize nodeId={node?.id}></DynamicCategorize>
<Output list={outputList}></Output>
</FormWrapper>
</Form>
);
}
export default memo(CategorizeForm);

View File

@ -0,0 +1,32 @@
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
export function useCreateCategorizeFormSchema() {
const { t } = useTranslation();
const FormSchema = z.object({
query: z.string().optional(),
parameter: z.string().optional(),
...LlmSettingSchema,
message_history_window_size: z.coerce.number(),
items: z.array(
z
.object({
name: z.string().min(1, t('flow.nameMessage')).trim(),
description: z.string().optional(),
uuid: z.string(),
examples: z
.array(
z.object({
value: z.string(),
}),
)
.optional(),
})
.optional(),
),
});
return FormSchema;
}

View File

@ -0,0 +1,34 @@
import { ModelVariableType } from '@/constants/knowledge';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { isEmpty, isPlainObject } from 'lodash';
import { useMemo } from 'react';
const defaultValues = {
parameter: ModelVariableType.Precise,
message_history_window_size: 1,
temperatureEnabled: true,
topPEnabled: true,
presencePenaltyEnabled: true,
frequencyPenaltyEnabled: true,
maxTokensEnabled: true,
items: [],
};
export function useValues(node?: RAGFlowNodeType) {
const values = useMemo(() => {
const formData = node?.data?.form;
if (isEmpty(formData)) {
return defaultValues;
}
if (isPlainObject(formData)) {
// const nextValues = {
// ...omit(formData, 'category_description'),
// items,
// };
return formData;
}
}, [node]);
return values;
}

View File

@ -0,0 +1,17 @@
import { useEffect } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import useGraphStore from '../../store';
export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) {
let values = useWatch({ control: form?.control });
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
useEffect(() => {
// Manually triggered form updates are synchronized to the canvas
if (id) {
values = form?.getValues();
updateNodeForm(id, { ...values, items: values.items?.slice() || [] });
}
}, [id, updateNodeForm, values]);
}

View File

@ -0,0 +1,168 @@
import Editor, { loader } from '@monaco-editor/react';
import { INextOperatorForm } from '../../interface';
import { FormContainer } from '@/components/form-container';
import { useIsDarkTheme } from '@/components/theme-provider';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { RAGFlowSelect } from '@/components/ui/select';
import { ProgrammingLanguage } from '@/constants/agent';
import { ICodeForm } from '@/interfaces/database/agent';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import {
DynamicInputVariable,
TypeOptions,
VariableTitle,
} from './next-variable';
import { FormSchema, FormSchemaType } from './schema';
import { useValues } from './use-values';
import {
useHandleLanguageChange,
useWatchFormChange,
} from './use-watch-change';
loader.config({ paths: { vs: '/vs' } });
const options = [
ProgrammingLanguage.Python,
ProgrammingLanguage.Javascript,
].map((x) => ({ value: x, label: x }));
const DynamicFieldName = 'outputs';
function CodeForm({ node }: INextOperatorForm) {
const formData = node?.data.form as ICodeForm;
const { t } = useTranslation();
const values = useValues(node);
const isDarkTheme = useIsDarkTheme();
const form = useForm<FormSchemaType>({
defaultValues: values,
resolver: zodResolver(FormSchema),
});
useWatchFormChange(node?.id, form);
const handleLanguageChange = useHandleLanguageChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<DynamicInputVariable
node={node}
title={t('flow.input')}
isOutputs={false}
></DynamicInputVariable>
<FormField
control={form.control}
name="script"
render={({ field }) => (
<FormItem>
<FormLabel className="flex items-center justify-between">
Code
<FormField
control={form.control}
name="lang"
render={({ field }) => (
<FormItem>
<FormControl>
<RAGFlowSelect
{...field}
onChange={(val) => {
field.onChange(val);
handleLanguageChange(val);
}}
options={options}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</FormLabel>
<FormControl>
<Editor
height={300}
theme={isDarkTheme ? 'vs-dark' : 'vs'}
language={formData.lang}
options={{
minimap: { enabled: false },
automaticLayout: true,
}}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{formData.lang === ProgrammingLanguage.Python ? (
<DynamicInputVariable
node={node}
title={'Return Values'}
name={DynamicFieldName}
isOutputs
></DynamicInputVariable>
) : (
<div>
<VariableTitle title={'Return Values'}></VariableTitle>
<FormContainer className="space-y-5">
<FormField
control={form.control}
name={`${DynamicFieldName}.name`}
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
{...field}
placeholder={t('common.pleaseInput')}
></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`${DynamicFieldName}.type`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Type</FormLabel>
<FormControl>
<RAGFlowSelect
placeholder={t('common.pleaseSelect')}
options={TypeOptions}
{...field}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</FormContainer>
</div>
)}
</FormWrapper>
<div className="p-5">
<Output list={buildOutputList(formData.outputs)}></Output>
</div>
</Form>
);
}
export default memo(CodeForm);

View File

@ -0,0 +1,128 @@
'use client';
import { FormContainer } from '@/components/form-container';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { BlockButton, Button } from '@/components/ui/button';
import {
FormControl,
FormField,
FormItem,
FormMessage,
} from '@/components/ui/form';
import { BlurInput } from '@/components/ui/input';
import { RAGFlowSelect } from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { X } from 'lucide-react';
import { ReactNode } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query';
interface IProps {
node?: RAGFlowNodeType;
name?: string;
isOutputs: boolean;
}
export const TypeOptions = [
'String',
'Number',
'Boolean',
'Array<String>',
'Array<Number>',
'Object',
].map((x) => ({ label: x, value: x }));
export function DynamicVariableForm({ name = 'arguments', isOutputs }: IProps) {
const { t } = useTranslation();
const form = useFormContext();
const { fields, remove, append } = useFieldArray({
name: name,
control: form.control,
});
const nextOptions = useBuildQueryVariableOptions();
return (
<div className="space-y-5">
{fields.map((field, index) => {
const typeField = `${name}.${index}.name`;
return (
<div key={field.id} className="flex w-full items-center gap-2">
<FormField
control={form.control}
name={typeField}
render={({ field }) => (
<FormItem className="flex-1 overflow-hidden">
<FormControl>
<BlurInput
{...field}
placeholder={t('common.pleaseInput')}
></BlurInput>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Separator className="w-3 text-text-secondary" />
<FormField
control={form.control}
name={`${name}.${index}.type`}
render={({ field }) => (
<FormItem className="flex-1 overflow-hidden">
<FormControl>
{isOutputs ? (
<RAGFlowSelect
placeholder={t('common.pleaseSelect')}
options={TypeOptions}
{...field}
></RAGFlowSelect>
) : (
<SelectWithSearch
options={nextOptions}
{...field}
></SelectWithSearch>
)}
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button variant={'ghost'} onClick={() => remove(index)}>
<X className="text-text-sub-title-invert " />
</Button>
</div>
);
})}
<BlockButton onClick={() => append({ name: '', type: undefined })}>
{t('flow.addVariable')}
</BlockButton>
</div>
);
}
export function VariableTitle({ title }: { title: ReactNode }) {
return <div className="font-medium text-text-primary pb-2">{title}</div>;
}
export function DynamicInputVariable({
node,
name,
title,
isOutputs = false,
}: IProps & { title: ReactNode }) {
return (
<section>
<VariableTitle title={title}></VariableTitle>
<FormContainer>
<DynamicVariableForm
node={node}
name={name}
isOutputs={isOutputs}
></DynamicVariableForm>
</FormContainer>
</section>
);
}

View File

@ -0,0 +1,14 @@
import { ProgrammingLanguage } from '@/constants/agent';
import { z } from 'zod';
export const FormSchema = z.object({
lang: z.enum([ProgrammingLanguage.Python, ProgrammingLanguage.Javascript]),
script: z.string(),
arguments: z.array(z.object({ name: z.string(), type: z.string() })),
outputs: z.union([
z.array(z.object({ name: z.string(), type: z.string() })).optional(),
z.object({ name: z.string(), type: z.string() }),
]),
});
export type FormSchemaType = z.infer<typeof FormSchema>;

View File

@ -0,0 +1,47 @@
import { ProgrammingLanguage } from '@/constants/agent';
import { ICodeForm } from '@/interfaces/database/agent';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { initialCodeValues } from '../../constant';
function convertToArray(args: Record<string, string>) {
return Object.entries(args).map(([key, value]) => ({
name: key,
type: value,
}));
}
type OutputsFormType = { name: string; type: string };
function convertOutputsToArray({ lang, outputs = {} }: ICodeForm) {
if (lang === ProgrammingLanguage.Python) {
return Object.entries(outputs).map(([key, val]) => ({
name: key,
type: val.type,
}));
}
return Object.entries(outputs).reduce<OutputsFormType>((pre, [key, val]) => {
pre.name = key;
pre.type = val.type;
return pre;
}, {} as OutputsFormType);
}
export function useValues(node?: RAGFlowNodeType) {
const values = useMemo(() => {
const formData = node?.data?.form;
if (isEmpty(formData)) {
return initialCodeValues;
}
return {
...formData,
arguments: convertToArray(formData.arguments),
outputs: convertOutputsToArray(formData),
};
}, [node?.data?.form]);
return values;
}

View File

@ -0,0 +1,95 @@
import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent';
import { ICodeForm } from '@/interfaces/database/agent';
import { isEmpty } from 'lodash';
import { useCallback, useEffect } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import useGraphStore from '../../store';
import { FormSchemaType } from './schema';
function convertToObject(list: FormSchemaType['arguments'] = []) {
return list.reduce<Record<string, string>>((pre, cur) => {
pre[cur.name] = cur.type;
return pre;
}, {});
}
type ArrayOutputs = Extract<FormSchemaType['outputs'], Array<any>>;
type ObjectOutputs = Exclude<FormSchemaType['outputs'], Array<any>>;
function convertOutputsToObject({ lang, outputs }: FormSchemaType) {
if (lang === ProgrammingLanguage.Python) {
return (outputs as ArrayOutputs).reduce<ICodeForm['outputs']>(
(pre, cur) => {
pre[cur.name] = {
value: '',
type: cur.type,
};
return pre;
},
{},
);
}
const outputsObject = outputs as ObjectOutputs;
if (isEmpty(outputsObject)) {
return {};
}
return {
[outputsObject.name]: {
value: '',
type: outputsObject.type,
},
};
}
export function useWatchFormChange(
id?: string,
form?: UseFormReturn<FormSchemaType>,
) {
let values = useWatch({ control: form?.control });
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
useEffect(() => {
// Manually triggered form updates are synchronized to the canvas
if (id) {
values = form?.getValues() || {};
let nextValues: any = {
...values,
arguments: convertToObject(
values?.arguments as FormSchemaType['arguments'],
),
outputs: convertOutputsToObject(values as FormSchemaType),
};
updateNodeForm(id, nextValues);
}
}, [form?.formState.isDirty, id, updateNodeForm, values]);
}
export function useHandleLanguageChange(
id?: string,
form?: UseFormReturn<FormSchemaType>,
) {
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
const handleLanguageChange = useCallback(
(lang: string) => {
if (id) {
const script = CodeTemplateStrMap[lang as ProgrammingLanguage];
form?.setValue('script', script);
form?.setValue(
'outputs',
(lang === ProgrammingLanguage.Python
? []
: {}) as FormSchemaType['outputs'],
);
updateNodeForm(id, script, ['script']);
}
},
[form, id, updateNodeForm],
);
return handleLanguageChange;
}

View File

@ -0,0 +1,32 @@
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { t } from 'i18next';
import { useFormContext } from 'react-hook-form';
interface IApiKeyFieldProps {
placeholder?: string;
}
export function ApiKeyField({ placeholder }: IApiKeyFieldProps) {
const form = useFormContext();
return (
<FormField
control={form.control}
name="api_key"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.apiKey')}</FormLabel>
<FormControl>
<Input type="password" {...field} placeholder={placeholder}></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}

View File

@ -0,0 +1,27 @@
import {
FormControl,
FormField,
FormItem,
FormLabel,
} from '@/components/ui/form';
import { Textarea } from '@/components/ui/textarea';
import { t } from 'i18next';
import { useFormContext } from 'react-hook-form';
export function DescriptionField() {
const form = useFormContext();
return (
<FormField
control={form.control}
name={`description`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{t('flow.description')}</FormLabel>
<FormControl>
<Textarea {...field}></Textarea>
</FormControl>
</FormItem>
)}
/>
);
}

View File

@ -0,0 +1,127 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Button, Collapse, Flex, Form, Input, Select } from 'antd';
import { PropsWithChildren, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useBuildVariableOptions } from '../../hooks/use-get-begin-query';
import styles from './index.less';
interface IProps {
node?: RAGFlowNodeType;
}
enum VariableType {
Reference = 'reference',
Input = 'input',
}
const getVariableName = (type: string) =>
type === VariableType.Reference ? 'component_id' : 'value';
const DynamicVariableForm = ({ node }: IProps) => {
const { t } = useTranslation();
const valueOptions = useBuildVariableOptions(node?.id, node?.parentId);
const form = Form.useFormInstance();
const options = [
{ value: VariableType.Reference, label: t('flow.reference') },
{ value: VariableType.Input, label: t('flow.text') },
];
const handleTypeChange = useCallback(
(name: number) => () => {
setTimeout(() => {
form.setFieldValue(['query', name, 'component_id'], undefined);
form.setFieldValue(['query', name, 'value'], undefined);
}, 0);
},
[form],
);
return (
<Form.List name="query">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Flex key={key} gap={10} align={'baseline'}>
<Form.Item
{...restField}
name={[name, 'type']}
className={styles.variableType}
>
<Select
options={options}
onChange={handleTypeChange(name)}
></Select>
</Form.Item>
<Form.Item noStyle dependencies={[name, 'type']}>
{({ getFieldValue }) => {
const type = getFieldValue(['query', name, 'type']);
return (
<Form.Item
{...restField}
name={[name, getVariableName(type)]}
className={styles.variableValue}
>
{type === VariableType.Reference ? (
<Select
placeholder={t('common.pleaseSelect')}
options={valueOptions}
></Select>
) : (
<Input placeholder={t('common.pleaseInput')} />
)}
</Form.Item>
);
}}
</Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} />
</Flex>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => add({ type: VariableType.Reference })}
block
icon={<PlusOutlined />}
className={styles.addButton}
>
{t('flow.addVariable')}
</Button>
</Form.Item>
</>
)}
</Form.List>
);
};
export function FormCollapse({
children,
title,
}: PropsWithChildren<{ title: string }>) {
return (
<Collapse
className={styles.dynamicInputVariable}
defaultActiveKey={['1']}
items={[
{
key: '1',
label: <span className={styles.title}>{title}</span>,
children,
},
]}
/>
);
}
const DynamicInputVariable = ({ node }: IProps) => {
const { t } = useTranslation();
return (
<FormCollapse title={t('flow.input')}>
<DynamicVariableForm node={node}></DynamicVariableForm>
</FormCollapse>
);
};
export default DynamicInputVariable;

View File

@ -0,0 +1,16 @@
type FormProps = React.ComponentProps<'form'>;
export function FormWrapper({ children, ...props }: FormProps) {
return (
<form
className="space-y-6 p-4"
autoComplete="off"
onSubmit={(e) => {
e.preventDefault();
}}
{...props}
>
{children}
</form>
);
}

View File

@ -0,0 +1,22 @@
.dynamicInputVariable {
background-color: #ebe9e950;
:global(.ant-collapse-content) {
background-color: #f6f6f657;
}
margin-bottom: 20px;
.title {
font-weight: 600;
font-size: 16px;
}
.variableType {
width: 30%;
}
.variableValue {
flex: 1;
}
.addButton {
color: rgb(22, 119, 255);
font-weight: 600;
}
}

View File

@ -0,0 +1,135 @@
'use client';
import { SideDown } from '@/assets/icon/next-icon';
import { Button } from '@/components/ui/button';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { RAGFlowSelect } from '@/components/ui/select';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { Plus, Trash2 } from 'lucide-react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useBuildVariableOptions } from '../../hooks/use-get-begin-query';
interface IProps {
node?: RAGFlowNodeType;
}
enum VariableType {
Reference = 'reference',
Input = 'input',
}
const getVariableName = (type: string) =>
type === VariableType.Reference ? 'component_id' : 'value';
export function DynamicVariableForm({ node }: IProps) {
const { t } = useTranslation();
const form = useFormContext();
const { fields, remove, append } = useFieldArray({
name: 'query',
control: form.control,
});
const valueOptions = useBuildVariableOptions(node?.id, node?.parentId);
const options = [
{ value: VariableType.Reference, label: t('flow.reference') },
{ value: VariableType.Input, label: t('flow.text') },
];
return (
<div>
{fields.map((field, index) => {
const typeField = `query.${index}.type`;
const typeValue = form.watch(typeField);
return (
<div key={field.id} className="flex items-center gap-1">
<FormField
control={form.control}
name={typeField}
render={({ field }) => (
<FormItem className="w-2/5">
<FormDescription />
<FormControl>
<RAGFlowSelect
{...field}
placeholder={t('common.pleaseSelect')}
options={options}
onChange={(val) => {
field.onChange(val);
form.resetField(`query.${index}.value`);
form.resetField(`query.${index}.component_id`);
}}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`query.${index}.${getVariableName(typeValue)}`}
render={({ field }) => (
<FormItem className="flex-1">
<FormDescription />
<FormControl>
{typeValue === VariableType.Reference ? (
<RAGFlowSelect
placeholder={t('common.pleaseSelect')}
{...field}
options={valueOptions}
></RAGFlowSelect>
) : (
<Input placeholder={t('common.pleaseInput')} {...field} />
)}
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Trash2
className="cursor-pointer mx-3 size-4 text-colors-text-functional-danger"
onClick={() => remove(index)}
/>
</div>
);
})}
<Button onClick={append} className="mt-4" variant={'outline'} size={'sm'}>
<Plus />
{t('flow.addVariable')}
</Button>
</div>
);
}
export function DynamicInputVariable({ node }: IProps) {
const { t } = useTranslation();
return (
<Collapsible defaultOpen className="group/collapsible">
<CollapsibleTrigger className="flex justify-between w-full pb-2">
<span className="font-bold text-2xl text-colors-text-neutral-strong">
{t('flow.input')}
</span>
<Button variant={'icon'} size={'icon'}>
<SideDown />
</Button>
</CollapsibleTrigger>
<CollapsibleContent>
<DynamicVariableForm node={node}></DynamicVariableForm>
</CollapsibleContent>
</Collapsible>
);
}

View File

@ -0,0 +1,35 @@
import { t } from 'i18next';
export type OutputType = {
title: string;
type?: string;
};
type OutputProps = {
list: Array<OutputType>;
};
export function transferOutputs(outputs: Record<string, any>) {
return Object.entries(outputs).map(([key, value]) => ({
title: key,
type: value?.type,
}));
}
export function Output({ list }: OutputProps) {
return (
<section className="space-y-2">
<div>{t('flow.output')}</div>
<ul>
{list.map((x, idx) => (
<li
key={idx}
className="bg-background-highlight text-accent-primary rounded-sm px-2 py-1"
>
{x.title}: <span className="text-text-secondary">{x.type}</span>
</li>
))}
</ul>
</section>
);
}

View File

@ -0,0 +1 @@
export const ProgrammaticTag = 'programmatic';

View File

@ -0,0 +1,76 @@
.typeahead-popover {
background: #fff;
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
border-radius: 8px;
position: fixed;
z-index: 1000;
}
.typeahead-popover ul {
list-style: none;
margin: 0;
max-height: 200px;
overflow-y: scroll;
}
.typeahead-popover ul::-webkit-scrollbar {
display: none;
}
.typeahead-popover ul {
-ms-overflow-style: none;
scrollbar-width: none;
}
.typeahead-popover ul li {
margin: 0;
min-width: 180px;
font-size: 14px;
outline: none;
cursor: pointer;
border-radius: 8px;
}
.typeahead-popover ul li.selected {
background: #eee;
}
.typeahead-popover li {
margin: 0 8px 0 8px;
color: #050505;
cursor: pointer;
line-height: 16px;
font-size: 15px;
display: flex;
align-content: center;
flex-direction: row;
flex-shrink: 0;
background-color: #fff;
border: 0;
}
.typeahead-popover li.active {
display: flex;
width: 20px;
height: 20px;
background-size: contain;
}
.typeahead-popover li .text {
display: flex;
line-height: 20px;
flex-grow: 1;
min-width: 150px;
}
.typeahead-popover li .icon {
display: flex;
width: 20px;
height: 20px;
user-select: none;
margin-right: 8px;
line-height: 16px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}

View File

@ -0,0 +1,181 @@
import { CodeHighlightNode, CodeNode } from '@lexical/code';
import {
InitialConfigType,
LexicalComposer,
} from '@lexical/react/LexicalComposer';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
import {
$getRoot,
$getSelection,
EditorState,
Klass,
LexicalNode,
} from 'lexical';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { Variable } from 'lucide-react';
import { ReactNode, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PasteHandlerPlugin } from './paste-handler-plugin';
import theme from './theme';
import { VariableNode } from './variable-node';
import { VariableOnChangePlugin } from './variable-on-change-plugin';
import VariablePickerMenuPlugin from './variable-picker-plugin';
// Catch any errors that occur during Lexical updates and log them
// or throw them as needed. If you don't throw them, Lexical will
// try to recover gracefully without losing user data.
function onError(error: Error) {
console.error(error);
}
const Nodes: Array<Klass<LexicalNode>> = [
HeadingNode,
QuoteNode,
CodeHighlightNode,
CodeNode,
VariableNode,
];
type PromptContentProps = { showToolbar?: boolean; multiLine?: boolean };
type IProps = {
value?: string;
onChange?: (value?: string) => void;
placeholder?: ReactNode;
} & PromptContentProps;
function PromptContent({
showToolbar = true,
multiLine = true,
}: PromptContentProps) {
const [editor] = useLexicalComposerContext();
const [isBlur, setIsBlur] = useState(false);
const { t } = useTranslation();
const insertTextAtCursor = useCallback(() => {
editor.update(() => {
const selection = $getSelection();
if (selection !== null) {
selection.insertText(' /');
}
});
}, [editor]);
const handleVariableIconClick = useCallback(() => {
insertTextAtCursor();
}, [insertTextAtCursor]);
const handleBlur = useCallback(() => {
setIsBlur(true);
}, []);
const handleFocus = useCallback(() => {
setIsBlur(false);
}, []);
return (
<section
className={cn('border rounded-sm ', { 'border-blue-400': !isBlur })}
>
{showToolbar && (
<div className="border-b px-2 py-2 justify-end flex">
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-block cursor-pointer cursor p-0.5 hover:bg-gray-100 dark:hover:bg-slate-800 rounded-sm">
<Variable size={16} onClick={handleVariableIconClick} />
</span>
</TooltipTrigger>
<TooltipContent>
<p>{t('flow.insertVariableTip')}</p>
</TooltipContent>
</Tooltip>
</div>
)}
<ContentEditable
className={cn(
'relative px-2 py-1 focus-visible:outline-none max-h-[50vh] overflow-auto',
{
'min-h-40': multiLine,
},
)}
onBlur={handleBlur}
onFocus={handleFocus}
/>
</section>
);
}
export function PromptEditor({
value,
onChange,
placeholder,
showToolbar,
multiLine = true,
}: IProps) {
const { t } = useTranslation();
const initialConfig: InitialConfigType = {
namespace: 'PromptEditor',
theme,
onError,
nodes: Nodes,
};
const onValueChange = useCallback(
(editorState: EditorState) => {
editorState?.read(() => {
// const listNodes = $nodesOfType(VariableNode); // to be removed
// const allNodes = $dfs();
const text = $getRoot().getTextContent();
onChange?.(text);
});
},
[onChange],
);
return (
<div className="relative">
<LexicalComposer initialConfig={initialConfig}>
<RichTextPlugin
contentEditable={
<PromptContent
showToolbar={showToolbar}
multiLine={multiLine}
></PromptContent>
}
placeholder={
<div
className={cn(
'absolute top-1 left-2 text-text-secondary pointer-events-none',
{
'truncate w-[90%]': !multiLine,
'translate-y-10': multiLine,
},
)}
>
{placeholder || t('common.promptPlaceholder')}
</div>
}
ErrorBoundary={LexicalErrorBoundary}
/>
<VariablePickerMenuPlugin value={value}></VariablePickerMenuPlugin>
<PasteHandlerPlugin />
<VariableOnChangePlugin
onChange={onValueChange}
></VariableOnChangePlugin>
</LexicalComposer>
</div>
);
}

View File

@ -0,0 +1,83 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import {
$createParagraphNode,
$createTextNode,
$getSelection,
$isRangeSelection,
PASTE_COMMAND,
} from 'lexical';
import { useEffect } from 'react';
function PasteHandlerPlugin() {
const [editor] = useLexicalComposerContext();
useEffect(() => {
const removeListener = editor.registerCommand(
PASTE_COMMAND,
(clipboardEvent: ClipboardEvent) => {
const clipboardData = clipboardEvent.clipboardData;
if (!clipboardData) {
return false;
}
const text = clipboardData.getData('text/plain');
if (!text) {
return false;
}
// Check if text contains line breaks
if (text.includes('\n')) {
editor.update(() => {
const selection = $getSelection();
if (selection && $isRangeSelection(selection)) {
// Normalize line breaks, merge multiple consecutive line breaks into a single line break
const normalizedText = text.replace(/\n{2,}/g, '\n');
// Clear current selection
selection.removeText();
// Create a paragraph node to contain all content
const paragraph = $createParagraphNode();
// Split text by line breaks
const lines = normalizedText.split('\n');
// Process each line
lines.forEach((lineText, index) => {
// Add line text (if any)
if (lineText) {
const textNode = $createTextNode(lineText);
paragraph.append(textNode);
}
// If not the last line, add a line break
if (index < lines.length - 1) {
const lineBreak = $createTextNode('\n');
paragraph.append(lineBreak);
}
});
// Insert paragraph
selection.insertNodes([paragraph]);
}
});
// Prevent default paste behavior
clipboardEvent.preventDefault();
return true;
}
// If no line breaks, use default behavior
return false;
},
4,
);
return () => {
removeListener();
};
}, [editor]);
return null;
}
export { PasteHandlerPlugin };

View File

@ -0,0 +1,43 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
export default {
code: 'editor-code',
heading: {
h1: 'editor-heading-h1',
h2: 'editor-heading-h2',
h3: 'editor-heading-h3',
h4: 'editor-heading-h4',
h5: 'editor-heading-h5',
},
image: 'editor-image',
link: 'editor-link',
list: {
listitem: 'editor-listitem',
nested: {
listitem: 'editor-nested-listitem',
},
ol: 'editor-list-ol',
ul: 'editor-list-ul',
},
ltr: 'ltr',
paragraph: 'editor-paragraph',
placeholder: 'editor-placeholder',
quote: 'editor-quote',
rtl: 'rtl',
text: {
bold: 'editor-text-bold',
code: 'editor-text-code',
hashtag: 'editor-text-hashtag',
italic: 'editor-text-italic',
overflowed: 'editor-text-overflowed',
strikethrough: 'editor-text-strikethrough',
underline: 'editor-text-underline',
underlineStrikethrough: 'editor-text-underlineStrikethrough',
},
};

View File

@ -0,0 +1,91 @@
import { BeginId } from '@/pages/flow/constant';
import { DecoratorNode, LexicalNode, NodeKey } from 'lexical';
import { ReactNode } from 'react';
const prefix = BeginId + '@';
export class VariableNode extends DecoratorNode<ReactNode> {
__value: string;
__label: string;
key?: NodeKey;
__parentLabel?: string | ReactNode;
__icon?: ReactNode;
static getType(): string {
return 'variable';
}
static clone(node: VariableNode): VariableNode {
return new VariableNode(
node.__value,
node.__label,
node.__key,
node.__parentLabel,
node.__icon,
);
}
constructor(
value: string,
label: string,
key?: NodeKey,
parent?: string | ReactNode,
icon?: ReactNode,
) {
super(key);
this.__value = value;
this.__label = label;
this.__parentLabel = parent;
this.__icon = icon;
}
createDOM(): HTMLElement {
const dom = document.createElement('span');
dom.className = 'mr-1';
return dom;
}
updateDOM(): false {
return false;
}
decorate(): ReactNode {
let content: ReactNode = (
<div className="text-blue-600">{this.__label}</div>
);
if (this.__parentLabel) {
content = (
<div className="flex items-center gap-1 text-text-primary ">
<div>{this.__icon}</div>
<div>{this.__parentLabel}</div>
<div className="text-text-disabled mr-1">/</div>
{content}
</div>
);
}
return (
<div className="bg-gray-200 dark:bg-gray-400 text-sm inline-flex items-center rounded-md px-2 py-1">
{content}
</div>
);
}
getTextContent(): string {
return `{${this.__value}}`;
}
}
export function $createVariableNode(
value: string,
label: string,
parentLabel: string | ReactNode,
icon?: ReactNode,
): VariableNode {
return new VariableNode(value, label, undefined, parentLabel, icon);
}
export function $isVariableNode(
node: LexicalNode | null | undefined,
): node is VariableNode {
return node instanceof VariableNode;
}

View File

@ -0,0 +1,35 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { EditorState, LexicalEditor } from 'lexical';
import { useEffect } from 'react';
import { ProgrammaticTag } from './constant';
interface IProps {
onChange: (
editorState: EditorState,
editor?: LexicalEditor,
tags?: Set<string>,
) => void;
}
export function VariableOnChangePlugin({ onChange }: IProps) {
// Access the editor through the LexicalComposerContext
const [editor] = useLexicalComposerContext();
// Wrap our listener in useEffect to handle the teardown and avoid stale references.
useEffect(() => {
// most listeners return a teardown function that can be called to clean them up.
return editor.registerUpdateListener(
({ editorState, tags, dirtyElements }) => {
// Check if there is a "programmatic" tag
const isProgrammaticUpdate = tags.has(ProgrammaticTag);
// The onchange event is only triggered when the data is manually updated
// Otherwise, the content will be displayed incorrectly.
if (dirtyElements.size > 0 && !isProgrammaticUpdate) {
onChange(editorState);
}
},
);
}, [editor, onChange]);
return null;
}

View File

@ -0,0 +1,297 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import {
LexicalTypeaheadMenuPlugin,
MenuOption,
useBasicTypeaheadTriggerMatch,
} from '@lexical/react/LexicalTypeaheadMenuPlugin';
import {
$createParagraphNode,
$createTextNode,
$getRoot,
$getSelection,
$isRangeSelection,
TextNode,
} from 'lexical';
import React, {
ReactElement,
ReactNode,
useCallback,
useEffect,
useRef,
} from 'react';
import * as ReactDOM from 'react-dom';
import { $createVariableNode } from './variable-node';
import { useBuildQueryVariableOptions } from '@/pages/agent/hooks/use-get-begin-query';
import { ProgrammaticTag } from './constant';
import './index.css';
class VariableInnerOption extends MenuOption {
label: string;
value: string;
parentLabel: string | JSX.Element;
icon?: ReactNode;
constructor(
label: string,
value: string,
parentLabel: string | JSX.Element,
icon?: ReactNode,
) {
super(value);
this.label = label;
this.value = value;
this.parentLabel = parentLabel;
this.icon = icon;
}
}
class VariableOption extends MenuOption {
label: ReactElement | string;
title: string;
options: VariableInnerOption[];
constructor(
label: ReactElement | string,
title: string,
options: VariableInnerOption[],
) {
super(title);
this.label = label;
this.title = title;
this.options = options;
}
}
function VariablePickerMenuItem({
index,
option,
selectOptionAndCleanUp,
}: {
index: number;
option: VariableOption;
selectOptionAndCleanUp: (
option: VariableOption | VariableInnerOption,
) => void;
}) {
return (
<li
key={option.key}
tabIndex={-1}
ref={option.setRefElement}
role="option"
id={'typeahead-item-' + index}
>
<div>
<span className="text text-slate-500">{option.title}</span>
<ul className="pl-2 py-1">
{option.options.map((x) => (
<li
key={x.value}
onClick={() => selectOptionAndCleanUp(x)}
className="hover:bg-slate-300 p-1"
>
{x.label}
</li>
))}
</ul>
</div>
</li>
);
}
export default function VariablePickerMenuPlugin({
value,
}: {
value?: string;
}): JSX.Element {
const [editor] = useLexicalComposerContext();
const isFirstRender = useRef(true);
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
minLength: 0,
});
const [queryString, setQueryString] = React.useState<string | null>('');
const options = useBuildQueryVariableOptions();
const buildNextOptions = useCallback(() => {
let filteredOptions = options;
if (queryString) {
const lowerQuery = queryString.toLowerCase();
filteredOptions = options
.map((x) => ({
...x,
options: x.options.filter(
(y) =>
y.label.toLowerCase().includes(lowerQuery) ||
y.value.toLowerCase().includes(lowerQuery),
),
}))
.filter((x) => x.options.length > 0);
}
const nextOptions: VariableOption[] = filteredOptions.map(
(x) =>
new VariableOption(
x.label,
x.title,
x.options.map((y) => {
return new VariableInnerOption(y.label, y.value, x.label, y.icon);
}),
),
);
return nextOptions;
}, [options, queryString]);
const findItemByValue = useCallback(
(value: string) => {
const children = options.reduce<
Array<{
label: string;
value: string;
parentLabel?: string | ReactNode;
icon?: ReactNode;
}>
>((pre, cur) => {
return pre.concat(cur.options);
}, []);
return children.find((x) => x.value === value);
},
[options],
);
const onSelectOption = useCallback(
(
selectedOption: VariableOption | VariableInnerOption,
nodeToRemove: TextNode | null,
closeMenu: () => void,
) => {
editor.update(() => {
const selection = $getSelection();
if (!$isRangeSelection(selection) || selectedOption === null) {
return;
}
if (nodeToRemove) {
nodeToRemove.remove();
}
const variableNode = $createVariableNode(
(selectedOption as VariableInnerOption).value,
selectedOption.label as string,
selectedOption.parentLabel as string | ReactNode,
selectedOption.icon as ReactNode,
);
selection.insertNodes([variableNode]);
closeMenu();
});
},
[editor],
);
const parseTextToVariableNodes = useCallback(
(text: string) => {
const paragraph = $createParagraphNode();
// Regular expression to match content within {}
const regex = /{([^}]*)}/g;
let match;
let lastIndex = 0;
while ((match = regex.exec(text)) !== null) {
const { 1: content, index, 0: template } = match;
// Add the previous text part (if any)
if (index > lastIndex) {
const textNode = $createTextNode(text.slice(lastIndex, index));
paragraph.append(textNode);
}
// Add variable node or text node
const nodeItem = findItemByValue(content);
if (nodeItem) {
paragraph.append(
$createVariableNode(
content,
nodeItem.label,
nodeItem.parentLabel,
nodeItem.icon,
),
);
} else {
paragraph.append($createTextNode(template));
}
// Update index
lastIndex = regex.lastIndex;
}
// Add the last part of text (if any)
if (lastIndex < text.length) {
const textNode = $createTextNode(text.slice(lastIndex));
paragraph.append(textNode);
}
$getRoot().clear().append(paragraph);
if ($isRangeSelection($getSelection())) {
$getRoot().selectEnd();
}
},
[findItemByValue],
);
useEffect(() => {
if (editor && value && isFirstRender.current) {
isFirstRender.current = false;
editor.update(
() => {
parseTextToVariableNodes(value);
},
{ tag: ProgrammaticTag },
);
}
}, [parseTextToVariableNodes, editor, value]);
return (
<LexicalTypeaheadMenuPlugin<VariableOption | VariableInnerOption>
onQueryChange={setQueryString}
onSelectOption={onSelectOption}
triggerFn={checkForTriggerMatch}
options={buildNextOptions()}
menuRenderFn={(anchorElementRef, { selectOptionAndCleanUp }) => {
const nextOptions = buildNextOptions();
return anchorElementRef.current && nextOptions.length
? ReactDOM.createPortal(
<div className="typeahead-popover w-[200px] p-2">
<ul className="overflow-y-auto !scrollbar-thin overflow-x-hidden">
{nextOptions.map((option, i: number) => (
<VariablePickerMenuItem
index={i}
key={option.key}
option={option}
selectOptionAndCleanUp={selectOptionAndCleanUp}
/>
))}
</ul>
</div>,
anchorElementRef.current,
)
: null;
}}
/>
);
}

View File

@ -0,0 +1,66 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { toLower } from 'lodash';
import { ReactNode, useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { VariableType } from '../../constant';
import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query';
type QueryVariableProps = {
name?: string;
type?: VariableType;
label?: ReactNode;
};
export function QueryVariable({
name = 'query',
type,
label,
}: QueryVariableProps) {
const { t } = useTranslation();
const form = useFormContext();
const nextOptions = useBuildQueryVariableOptions();
const finalOptions = useMemo(() => {
return type
? nextOptions.map((x) => {
return {
...x,
options: x.options.filter((y) => toLower(y.type).includes(type)),
};
})
: nextOptions;
}, [nextOptions, type]);
return (
<FormField
control={form.control}
name={name}
render={({ field }) => (
<FormItem>
{label || (
<FormLabel tooltip={t('flow.queryTip')}>
{t('flow.query')}
</FormLabel>
)}
<FormControl>
<SelectWithSearch
options={finalOptions}
{...field}
allowClear
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}

View File

@ -0,0 +1,105 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { useTranslate } from '@/hooks/common-hooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo, useMemo } from 'react';
import { useForm, useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { initialCrawlerValues } from '../../constant';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { CrawlerResultOptions } from '../../options';
import { QueryVariable } from '../components/query-variable';
export function CrawlerProxyFormField() {
const { t } = useTranslate('flow');
const form = useFormContext();
return (
<FormField
control={form.control}
name="proxy"
render={({ field }) => (
<FormItem>
<FormLabel>{t('proxy')}</FormLabel>
<FormControl>
<Input placeholder="like: http://127.0.0.1:8888" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}
export function CrawlerExtractTypeFormField() {
const { t } = useTranslate('flow');
const form = useFormContext();
const crawlerResultOptions = useMemo(() => {
return CrawlerResultOptions.map((x) => ({
value: x,
label: t(`crawlerResultOptions.${x}`),
}));
}, [t]);
return (
<FormField
control={form.control}
name="extract_type"
render={({ field }) => (
<FormItem>
<FormLabel>{t('extractType')}</FormLabel>
<FormControl>
<SelectWithSearch {...field} options={crawlerResultOptions} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}
export const CrawlerFormSchema = {
proxy: z.string().url(),
extract_type: z.string(),
};
const FormSchema = z.object({
query: z.string().optional(),
...CrawlerFormSchema,
});
function CrawlerForm({ node }: INextOperatorForm) {
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: initialCrawlerValues,
mode: 'onChange',
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<form
className="space-y-6 p-4"
onSubmit={(e) => {
e.preventDefault();
}}
>
<QueryVariable></QueryVariable>
<CrawlerProxyFormField></CrawlerProxyFormField>
<CrawlerExtractTypeFormField></CrawlerExtractTypeFormField>
</form>
</Form>
);
}
export default memo(CrawlerForm);

View File

@ -0,0 +1,36 @@
import TopNItem from '@/components/top-n-item';
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Select } from 'antd';
import { useBuildSortOptions } from '../../form-hooks';
import { IOperatorForm } from '../../interface';
import { DeepLSourceLangOptions, DeepLTargetLangOptions } from '../../options';
import DynamicInputVariable from '../components/dynamic-input-variable';
const DeepLForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const options = useBuildSortOptions();
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<TopNItem initialValue={5}></TopNItem>
<Form.Item label={t('authKey')} name={'auth_key'}>
<Select options={options}></Select>
</Form.Item>
<Form.Item label={t('sourceLang')} name={'source_lang'}>
<Select options={DeepLSourceLangOptions}></Select>
</Form.Item>
<Form.Item label={t('targetLang')} name={'target_lang'}>
<Select options={DeepLTargetLangOptions}></Select>
</Form.Item>
</Form>
);
};
export default DeepLForm;

View File

@ -0,0 +1,91 @@
import { FormContainer } from '@/components/form-container';
import { TopNFormField } from '@/components/top-n-item';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { RAGFlowSelect } from '@/components/ui/select';
import { useTranslate } from '@/hooks/common-hooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo, useMemo } from 'react';
import { useForm, useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { Channel, initialDuckValues } from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { QueryVariable } from '../components/query-variable';
export const DuckDuckGoFormPartialSchema = {
top_n: z.string(),
channel: z.string(),
};
const FormSchema = z.object({
query: z.string(),
...DuckDuckGoFormPartialSchema,
});
export function DuckDuckGoWidgets() {
const { t } = useTranslate('flow');
const form = useFormContext();
const options = useMemo(() => {
return Object.values(Channel).map((x) => ({ value: x, label: t(x) }));
}, [t]);
return (
<>
<TopNFormField></TopNFormField>
<FormField
control={form.control}
name={'channel'}
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('channelTip')}>{t('channel')}</FormLabel>
<FormControl>
<RAGFlowSelect {...field} options={options} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
);
}
const outputList = buildOutputList(initialDuckValues.outputs);
function DuckDuckGoForm({ node }: INextOperatorForm) {
const defaultValues = useFormValues(initialDuckValues, node);
const form = useForm<z.infer<typeof FormSchema>>({
defaultValues,
resolver: zodResolver(FormSchema),
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<QueryVariable></QueryVariable>
<DuckDuckGoWidgets></DuckDuckGoWidgets>
</FormContainer>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
}
export default memo(DuckDuckGoForm);

View File

@ -0,0 +1,161 @@
import { FormContainer } from '@/components/form-container';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { useTranslate } from '@/hooks/common-hooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { ReactNode } from 'react';
import { useForm, useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { initialEmailValues } from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { PromptEditor } from '../components/prompt-editor';
interface InputFormFieldProps {
name: string;
label: ReactNode;
type?: string;
}
function InputFormField({ name, label, type }: InputFormFieldProps) {
const form = useFormContext();
return (
<FormField
control={form.control}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<Input {...field} type={type}></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}
function PromptFormField({ name, label }: InputFormFieldProps) {
const form = useFormContext();
return (
<FormField
control={form.control}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<PromptEditor
{...field}
showToolbar={false}
multiLine={false}
></PromptEditor>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}
export function EmailFormWidgets() {
const { t } = useTranslate('flow');
return (
<>
<InputFormField
name="smtp_server"
label={t('smtpServer')}
></InputFormField>
<InputFormField
name="smtp_port"
label={t('smtpPort')}
type="number"
></InputFormField>
<InputFormField name="email" label={t('senderEmail')}></InputFormField>
<InputFormField
name="password"
label={t('authCode')}
type="password"
></InputFormField>
<InputFormField
name="sender_name"
label={t('senderName')}
></InputFormField>
</>
);
}
export const EmailFormPartialSchema = {
smtp_server: z.string(),
smtp_port: z.number(),
email: z.string(),
password: z.string(),
sender_name: z.string(),
};
const FormSchema = z.object({
to_email: z.string(),
cc_email: z.string(),
content: z.string(),
subject: z.string(),
...EmailFormPartialSchema,
});
const outputList = buildOutputList(initialEmailValues.outputs);
const EmailForm = ({ node }: INextOperatorForm) => {
const { t } = useTranslate('flow');
const defaultValues = useFormValues(initialEmailValues, node);
const form = useForm<z.infer<typeof FormSchema>>({
defaultValues,
resolver: zodResolver(FormSchema),
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<PromptFormField
name="to_email"
label={t('toEmail')}
></PromptFormField>
<PromptFormField
name="cc_email"
label={t('ccEmail')}
></PromptFormField>
<PromptFormField
name="content"
label={t('content')}
></PromptFormField>
<PromptFormField
name="subject"
label={t('subject')}
></PromptFormField>
<EmailFormWidgets></EmailFormWidgets>
</FormContainer>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
};
export default EmailForm;

View File

@ -0,0 +1,167 @@
import NumberInput from '@/components/originui/number-input';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { ButtonLoading } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { useTranslate } from '@/hooks/common-hooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo } from 'react';
import { useForm, useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { initialExeSqlValues } from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { ExeSQLOptions } from '../../options';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { QueryVariable } from '../components/query-variable';
import { FormSchema, useSubmitForm } from './use-submit-form';
const outputList = buildOutputList(initialExeSqlValues.outputs);
export function ExeSQLFormWidgets({ loading }: { loading: boolean }) {
const form = useFormContext();
const { t } = useTranslate('flow');
return (
<>
<FormField
control={form.control}
name="db_type"
render={({ field }) => (
<FormItem>
<FormLabel>{t('dbType')}</FormLabel>
<FormControl>
<SelectWithSearch
{...field}
options={ExeSQLOptions}
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="database"
render={({ field }) => (
<FormItem>
<FormLabel>{t('database')}</FormLabel>
<FormControl>
<Input {...field}></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>{t('username')}</FormLabel>
<FormControl>
<Input {...field}></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="host"
render={({ field }) => (
<FormItem>
<FormLabel>{t('host')}</FormLabel>
<FormControl>
<Input {...field}></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="port"
render={({ field }) => (
<FormItem>
<FormLabel>{t('port')}</FormLabel>
<FormControl>
<NumberInput {...field} className="w-full"></NumberInput>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t('password')}</FormLabel>
<FormControl>
<Input {...field} type="password"></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="max_records"
render={({ field }) => (
<FormItem>
<FormLabel>{t('maxRecords')}</FormLabel>
<FormControl>
<NumberInput {...field} className="w-full"></NumberInput>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<ButtonLoading loading={loading} type="submit">
{t('test')}
</ButtonLoading>
</div>
</>
);
}
function ExeSQLForm({ node }: INextOperatorForm) {
const defaultValues = useFormValues(initialExeSqlValues, node);
const { onSubmit, loading } = useSubmitForm();
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues,
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper onSubmit={form.handleSubmit(onSubmit)}>
<QueryVariable name="sql"></QueryVariable>
<ExeSQLFormWidgets loading={loading}></ExeSQLFormWidgets>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
}
export default memo(ExeSQLForm);

View File

@ -0,0 +1,31 @@
import { useTestDbConnect } from '@/hooks/use-agent-request';
import { useCallback } from 'react';
import { z } from 'zod';
export const ExeSQLFormSchema = {
db_type: z.string().min(1),
database: z.string().min(1),
username: z.string().min(1),
host: z.string().min(1),
port: z.number(),
password: z.string().min(1),
max_records: z.number(),
};
export const FormSchema = z.object({
sql: z.string().optional(),
...ExeSQLFormSchema,
});
export function useSubmitForm() {
const { testDbConnect, loading } = useTestDbConnect();
const onSubmit = useCallback(
async (data: z.infer<typeof FormSchema>) => {
testDbConnect(data);
},
[testDbConnect],
);
return { loading, onSubmit };
}

View File

@ -0,0 +1,52 @@
import { FormContainer } from '@/components/form-container';
import { TopNFormField } from '@/components/top-n-item';
import { Form } from '@/components/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { initialGithubValues } from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { QueryVariable } from '../components/query-variable';
export const FormSchema = z.object({
query: z.string(),
top_n: z.number(),
});
const outputList = buildOutputList(initialGithubValues.outputs);
function GithubForm({ node }: INextOperatorForm) {
const defaultValues = useFormValues(initialGithubValues, node);
const form = useForm<z.infer<typeof FormSchema>>({
defaultValues,
resolver: zodResolver(FormSchema),
mode: 'onChange',
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<QueryVariable></QueryVariable>
</FormContainer>
<FormContainer>
<TopNFormField></TopNFormField>
</FormContainer>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
}
export default memo(GithubForm);

View File

@ -0,0 +1,139 @@
import { FormContainer } from '@/components/form-container';
import NumberInput from '@/components/originui/number-input';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { useTranslate } from '@/hooks/common-hooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm, useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { initialGoogleValues } from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { GoogleCountryOptions, GoogleLanguageOptions } from '../../options';
import { buildOutputList } from '../../utils/build-output-list';
import { ApiKeyField } from '../components/api-key-field';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { QueryVariable } from '../components/query-variable';
const outputList = buildOutputList(initialGoogleValues.outputs);
export const GoogleFormPartialSchema = {
api_key: z.string(),
country: z.string(),
language: z.string(),
};
export const FormSchema = z.object({
...GoogleFormPartialSchema,
q: z.string(),
start: z.number(),
num: z.number(),
});
export function GoogleFormWidgets() {
const form = useFormContext();
const { t } = useTranslate('flow');
return (
<>
<FormField
control={form.control}
name={`country`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{t('country')}</FormLabel>
<FormControl>
<SelectWithSearch
{...field}
options={GoogleCountryOptions}
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`language`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{t('language')}</FormLabel>
<FormControl>
<SelectWithSearch
{...field}
options={GoogleLanguageOptions}
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
);
}
const GoogleForm = ({ node }: INextOperatorForm) => {
const { t } = useTranslate('flow');
const defaultValues = useFormValues(initialGoogleValues, node);
const form = useForm<z.infer<typeof FormSchema>>({
defaultValues,
resolver: zodResolver(FormSchema),
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<QueryVariable name="q"></QueryVariable>
</FormContainer>
<FormContainer>
<ApiKeyField placeholder={t('apiKeyPlaceholder')}></ApiKeyField>
<FormField
control={form.control}
name={`start`}
render={({ field }) => (
<FormItem>
<FormLabel>{t('flowStart')}</FormLabel>
<FormControl>
<NumberInput {...field} className="w-full"></NumberInput>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`num`}
render={({ field }) => (
<FormItem>
<FormLabel>{t('flowNum')}</FormLabel>
<FormControl>
<NumberInput {...field} className="w-full"></NumberInput>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<GoogleFormWidgets></GoogleFormWidgets>
</FormContainer>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
};
export default GoogleForm;

View File

@ -0,0 +1,166 @@
import { FormContainer } from '@/components/form-container';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { TopNFormField } from '@/components/top-n-item';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Switch } from '@/components/ui/switch';
import { useTranslate } from '@/hooks/common-hooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { DatePicker, DatePickerProps } from 'antd';
import dayjs from 'dayjs';
import { memo, useCallback, useMemo } from 'react';
import { useForm, useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { initialGoogleScholarValues } from '../../constant';
import { useBuildSortOptions } from '../../form-hooks';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { QueryVariable } from '../components/query-variable';
// TODO: To be replaced
const YearPicker = ({
onChange,
value,
}: {
onChange?: (val: number | undefined) => void;
value?: number | undefined;
}) => {
const handleChange: DatePickerProps['onChange'] = useCallback(
(val: any) => {
const nextVal = val?.format('YYYY');
onChange?.(nextVal ? Number(nextVal) : undefined);
},
[onChange],
);
// The year needs to be converted into a number and saved to the backend
const nextValue = useMemo(() => {
if (value) {
return dayjs(value.toString());
}
return undefined;
}, [value]);
return <DatePicker picker="year" onChange={handleChange} value={nextValue} />;
};
export function GoogleScholarFormWidgets() {
const form = useFormContext();
const { t } = useTranslate('flow');
const options = useBuildSortOptions();
return (
<>
<TopNFormField></TopNFormField>
<FormField
control={form.control}
name={`sort_by`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{t('sortBy')}</FormLabel>
<FormControl>
<SelectWithSearch {...field} options={options}></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`year_low`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{t('yearLow')}</FormLabel>
<FormControl>
<YearPicker {...field}></YearPicker>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`year_high`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{t('yearHigh')}</FormLabel>
<FormControl>
<YearPicker {...field}></YearPicker>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`patents`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>{t('patents')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
></Switch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
);
}
export const GoogleScholarFormPartialSchema = {
top_n: z.number(),
sort_by: z.string(),
year_low: z.number(),
year_high: z.number(),
patents: z.boolean(),
};
export const FormSchema = z.object({
...GoogleScholarFormPartialSchema,
query: z.string(),
});
const outputList = buildOutputList(initialGoogleScholarValues.outputs);
function GoogleScholarForm({ node }: INextOperatorForm) {
const defaultValues = useFormValues(initialGoogleScholarValues, node);
const form = useForm<z.infer<typeof FormSchema>>({
defaultValues,
resolver: zodResolver(FormSchema),
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<QueryVariable></QueryVariable>
</FormContainer>
<FormContainer>
<GoogleScholarFormWidgets></GoogleScholarFormWidgets>
</FormContainer>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
}
export default memo(GoogleScholarForm);

View File

@ -0,0 +1,97 @@
import get from 'lodash/get';
import {
ChangeEventHandler,
MouseEventHandler,
useCallback,
useMemo,
} from 'react';
import { v4 as uuid } from 'uuid';
import { IGenerateParameter, IInvokeVariable } from '../../interface';
import useGraphStore from '../../store';
export const useHandleOperateParameters = (nodeId: string) => {
const { getNode, updateNodeForm } = useGraphStore((state) => state);
const node = getNode(nodeId);
const dataSource: IGenerateParameter[] = useMemo(
() => get(node, 'data.form.variables', []) as IGenerateParameter[],
[node],
);
const changeValue = useCallback(
(row: IInvokeVariable, field: string, value: string) => {
const newData = [...dataSource];
const index = newData.findIndex((item) => row.id === item.id);
const item = newData[index];
newData.splice(index, 1, {
...item,
[field]: value,
});
updateNodeForm(nodeId, { variables: newData });
},
[dataSource, nodeId, updateNodeForm],
);
const handleComponentIdChange = useCallback(
(row: IInvokeVariable) => (value: string) => {
changeValue(row, 'component_id', value);
},
[changeValue],
);
const handleValueChange = useCallback(
(row: IInvokeVariable): ChangeEventHandler<HTMLInputElement> =>
(e) => {
changeValue(row, 'value', e.target.value);
},
[changeValue],
);
const handleRemove = useCallback(
(id?: string) => () => {
const newData = dataSource.filter((item) => item.id !== id);
updateNodeForm(nodeId, { variables: newData });
},
[updateNodeForm, nodeId, dataSource],
);
const handleAdd: MouseEventHandler = useCallback(
(e) => {
e.preventDefault();
e.stopPropagation();
updateNodeForm(nodeId, {
variables: [
...dataSource,
{
id: uuid(),
key: '',
component_id: undefined,
value: '',
},
],
});
},
[dataSource, nodeId, updateNodeForm],
);
const handleSave = (row: IGenerateParameter) => {
const newData = [...dataSource];
const index = newData.findIndex((item) => row.id === item.id);
const item = newData[index];
newData.splice(index, 1, {
...item,
...row,
});
updateNodeForm(nodeId, { variables: newData });
};
return {
handleAdd,
handleRemove,
handleComponentIdChange,
handleValueChange,
handleSave,
dataSource,
};
};

View File

@ -0,0 +1,226 @@
import { Collapse } from '@/components/collapse';
import { FormContainer } from '@/components/form-container';
import NumberInput from '@/components/originui/number-input';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { zodResolver } from '@hookform/resolvers/zod';
import Editor, { loader } from '@monaco-editor/react';
import { Plus } from 'lucide-react';
import { memo } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { initialInvokeValues } from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { FormSchema, FormSchemaType } from './schema';
import { useEditVariableRecord } from './use-edit-variable';
import { VariableDialog } from './variable-dialog';
import { VariableTable } from './variable-table';
loader.config({ paths: { vs: '/vs' } });
enum Method {
GET = 'GET',
POST = 'POST',
PUT = 'PUT',
}
const MethodOptions = [Method.GET, Method.POST, Method.PUT].map((x) => ({
label: x,
value: x,
}));
interface TimeoutInputProps {
value?: number;
onChange?: (value: number | null) => void;
}
const TimeoutInput = ({ value, onChange }: TimeoutInputProps) => {
const { t } = useTranslation();
return (
<div className="flex gap-2 items-center">
<NumberInput value={value} onChange={onChange} /> {t('flow.seconds')}
</div>
);
};
const outputList = buildOutputList(initialInvokeValues.outputs);
function InvokeForm({ node }: INextOperatorForm) {
const { t } = useTranslation();
const defaultValues = useFormValues(initialInvokeValues, node);
const form = useForm<FormSchemaType>({
defaultValues,
resolver: zodResolver(FormSchema),
mode: 'onChange',
});
const {
visible,
hideModal,
showModal,
ok,
currentRecord,
otherThanCurrentQuery,
handleDeleteRecord,
} = useEditVariableRecord({
form,
node,
});
const variables = useWatch({ control: form.control, name: 'variables' });
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<FormField
control={form.control}
name="url"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.url')}</FormLabel>
<FormControl>
<Input {...field} placeholder="http://" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="method"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.method')}</FormLabel>
<FormControl>
<SelectWithSearch {...field} options={MethodOptions} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="timeout"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.timeout')}</FormLabel>
<FormControl>
<TimeoutInput {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="headers"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.headers')}</FormLabel>
<FormControl>
<Editor
height={200}
defaultLanguage="json"
theme="vs-dark"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="proxy"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.proxy')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="clean_html"
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('flow.cleanHtmlTip')}>
{t('flow.cleanHtml')}
</FormLabel>
<FormControl>
<Switch
onCheckedChange={field.onChange}
checked={field.value}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Create a hidden field to make Form instance record this */}
<FormField
control={form.control}
name={'variables'}
render={() => <div></div>}
/>
</FormContainer>
<Collapse
title={<div>{t('flow.parameter')}</div>}
rightContent={
<Button
variant={'ghost'}
onClick={(e) => {
e.preventDefault();
showModal();
}}
>
<Plus />
</Button>
}
>
<VariableTable
data={variables}
showModal={showModal}
deleteRecord={handleDeleteRecord}
nodeId={node?.id}
></VariableTable>
</Collapse>
{visible && (
<VariableDialog
hideModal={hideModal}
initialValue={currentRecord}
otherThanCurrentQuery={otherThanCurrentQuery}
submit={ok}
></VariableDialog>
)}
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
}
export default memo(InvokeForm);

View File

@ -0,0 +1,21 @@
import { z } from 'zod';
export const VariableFormSchema = z.object({
key: z.string(),
ref: z.string(),
value: z.string(),
});
export const FormSchema = z.object({
url: z.string().url(),
method: z.string(),
timeout: z.number(),
headers: z.string(),
proxy: z.string().url(),
clean_html: z.boolean(),
variables: z.array(VariableFormSchema),
});
export type FormSchemaType = z.infer<typeof FormSchema>;
export type VariableFormSchemaType = z.infer<typeof VariableFormSchema>;

View File

@ -0,0 +1,70 @@
import { useSetModalState } from '@/hooks/common-hooks';
import { useSetSelectedRecord } from '@/hooks/logic-hooks';
import { useCallback, useMemo, useState } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import { INextOperatorForm } from '../../interface';
import { FormSchemaType, VariableFormSchemaType } from './schema';
export const useEditVariableRecord = ({
form,
}: INextOperatorForm & { form: UseFormReturn<FormSchemaType> }) => {
const { setRecord, currentRecord } =
useSetSelectedRecord<VariableFormSchemaType>();
const { visible, hideModal, showModal } = useSetModalState();
const [index, setIndex] = useState(-1);
const variables = useWatch({
control: form.control,
name: 'variables',
});
const otherThanCurrentQuery = useMemo(() => {
return variables.filter((item, idx) => idx !== index);
}, [index, variables]);
const handleEditRecord = useCallback(
(record: VariableFormSchemaType) => {
const variables = form?.getValues('variables') || [];
const nextVaribales =
index > -1
? variables.toSpliced(index, 1, record)
: [...variables, record];
form.setValue('variables', nextVaribales);
hideModal();
},
[form, hideModal, index],
);
const handleShowModal = useCallback(
(idx?: number, record?: VariableFormSchemaType) => {
setIndex(idx ?? -1);
setRecord(record ?? ({} as VariableFormSchemaType));
showModal();
},
[setRecord, showModal],
);
const handleDeleteRecord = useCallback(
(idx: number) => {
const variables = form?.getValues('variables') || [];
const nextVariables = variables.filter((item, index) => index !== idx);
form.setValue('variables', nextVariables);
},
[form],
);
return {
ok: handleEditRecord,
currentRecord,
setRecord,
visible,
hideModal,
showModal: handleShowModal,
otherThanCurrentQuery,
handleDeleteRecord,
};
};

View File

@ -0,0 +1,143 @@
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { IModalProps } from '@/interfaces/common';
import { zodResolver } from '@hookform/resolvers/zod';
import { isEmpty } from 'lodash';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { QueryVariable } from '../components/query-variable';
import { VariableFormSchemaType } from './schema';
type ModalFormProps = {
initialValue: VariableFormSchemaType;
otherThanCurrentQuery: VariableFormSchemaType[];
submit(values: any): void;
};
const FormId = 'BeginParameterForm';
function VariableForm({
initialValue,
otherThanCurrentQuery,
submit,
}: ModalFormProps) {
const { t } = useTranslation();
const FormSchema = z.object({
key: z
.string()
.trim()
.min(1)
.refine(
(value) =>
!value || !otherThanCurrentQuery.some((x) => x.key === value),
{ message: 'The key cannot be repeated!' },
),
ref: z.string(),
value: z.string(),
});
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
mode: 'onChange',
defaultValues: {
key: '',
value: '',
ref: '',
},
});
useEffect(() => {
if (!isEmpty(initialValue)) {
form.reset(initialValue);
}
}, [form, initialValue]);
function onSubmit(data: z.infer<typeof FormSchema>) {
submit(data);
}
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id={FormId}
className="space-y-5"
autoComplete="off"
>
<FormField
name="key"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.key')}</FormLabel>
<FormControl>
<Input {...field} autoComplete="off" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<QueryVariable name="ref" label={t('flow.ref')}></QueryVariable>
<FormField
name="value"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.value')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
);
}
export function VariableDialog({
initialValue,
hideModal,
otherThanCurrentQuery,
submit,
}: ModalFormProps & IModalProps<VariableFormSchemaType>) {
const { t } = useTranslation();
return (
<Dialog open onOpenChange={hideModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('flow.variableSettings')}</DialogTitle>
</DialogHeader>
<VariableForm
initialValue={initialValue}
otherThanCurrentQuery={otherThanCurrentQuery}
submit={submit}
></VariableForm>
<DialogFooter>
<Button type="submit" form={FormId}>
{t('modal.okText')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,199 @@
'use client';
import {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import { Pencil, Trash2 } from 'lucide-react';
import * as React from 'react';
import { TableEmpty } from '@/components/table-skeleton';
import { Button } from '@/components/ui/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
import { useGetVariableLabelByValue } from '../../hooks/use-get-begin-query';
import { VariableFormSchemaType } from './schema';
interface IProps {
data: VariableFormSchemaType[];
deleteRecord(index: number): void;
showModal(index: number, record: VariableFormSchemaType): void;
nodeId?: string;
}
export function VariableTable({
data = [],
deleteRecord,
showModal,
nodeId,
}: IProps) {
const { t } = useTranslation();
const getLabel = useGetVariableLabelByValue(nodeId!);
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[],
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const columns: ColumnDef<VariableFormSchemaType>[] = [
{
accessorKey: 'key',
header: t('flow.key'),
meta: { cellClassName: 'max-w-30' },
cell: ({ row }) => {
const key: string = row.getValue('key');
return (
<Tooltip>
<TooltipTrigger asChild>
<div className="truncate">{key}</div>
</TooltipTrigger>
<TooltipContent>
<p>{key}</p>
</TooltipContent>
</Tooltip>
);
},
},
{
accessorKey: 'ref',
header: t('flow.ref'),
meta: { cellClassName: 'max-w-30' },
cell: ({ row }) => {
const ref: string = row.getValue('ref');
const label = getLabel(ref);
return (
<Tooltip>
<TooltipTrigger asChild>
<div className="truncate">{label}</div>
</TooltipTrigger>
<TooltipContent>
<p>{label}</p>
</TooltipContent>
</Tooltip>
);
},
},
{
accessorKey: 'value',
header: t('flow.value'),
cell: ({ row }) => <div>{row.getValue('value')}</div>,
},
{
id: 'actions',
enableHiding: false,
header: t('common.action'),
cell: ({ row }) => {
const record = row.original;
const idx = row.index;
return (
<div>
<Button
className="bg-transparent text-foreground hover:bg-muted-foreground hover:text-foreground"
onClick={() => showModal(idx, record)}
>
<Pencil />
</Button>
<Button
className="bg-transparent text-foreground hover:bg-muted-foreground hover:text-foreground"
onClick={() => deleteRecord(idx)}
>
<Trash2 />
</Button>
</div>
);
},
},
];
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
state: {
sorting,
columnFilters,
columnVisibility,
},
});
return (
<div className="w-full">
<div className="rounded-md border">
<Table rootClassName="rounded-md">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={cn(cell.column.columnDef.meta?.cellClassName)}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableEmpty columnsLength={columns.length}></TableEmpty>
)}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@ -0,0 +1,128 @@
'use client';
import { FormContainer } from '@/components/form-container';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { BlockButton, Button } from '@/components/ui/button';
import {
FormControl,
FormField,
FormItem,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { t } from 'i18next';
import { X } from 'lucide-react';
import { ReactNode, useCallback, useMemo } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useBuildSubNodeOutputOptions } from './use-build-options';
interface IProps {
node?: RAGFlowNodeType;
}
export function DynamicOutputForm({ node }: IProps) {
const { t } = useTranslation();
const form = useFormContext();
const options = useBuildSubNodeOutputOptions(node?.id);
const name = 'outputs';
const flatOptions = useMemo(() => {
return options.reduce<{ label: string; value: string; type: string }[]>(
(pre, cur) => {
pre.push(...cur.options);
return pre;
},
[],
);
}, [options]);
const findType = useCallback(
(val: string) => {
const type = flatOptions.find((x) => x.value === val)?.type;
if (type) {
return `Array<${type}>`;
}
},
[flatOptions],
);
const { fields, remove, append } = useFieldArray({
name: name,
control: form.control,
});
return (
<div className="space-y-5">
{fields.map((field, index) => {
const nameField = `${name}.${index}.name`;
const typeField = `${name}.${index}.type`;
return (
<div key={field.id} className="flex items-center gap-2">
<FormField
control={form.control}
name={nameField}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Input
{...field}
placeholder={t('common.pleaseInput')}
></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Separator className="w-3 text-text-secondary" />
<FormField
control={form.control}
name={`${name}.${index}.ref`}
render={({ field }) => (
<FormItem className="w-2/5">
<FormControl>
<SelectWithSearch
options={options}
{...field}
onChange={(val) => {
form.setValue(typeField, findType(val));
field.onChange(val);
}}
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={typeField}
render={() => <div></div>}
/>
<Button variant={'ghost'} onClick={() => remove(index)}>
<X className="text-text-sub-title-invert " />
</Button>
</div>
);
})}
<BlockButton onClick={() => append({ name: '', ref: undefined })}>
{t('common.add')}
</BlockButton>
</div>
);
}
export function VariableTitle({ title }: { title: ReactNode }) {
return <div className="font-medium text-text-primary pb-2">{title}</div>;
}
export function DynamicOutput({ node }: IProps) {
return (
<FormContainer>
<VariableTitle title={t('flow.output')}></VariableTitle>
<DynamicOutputForm node={node}></DynamicOutputForm>
</FormContainer>
);
}

View File

@ -0,0 +1,57 @@
import { FormContainer } from '@/components/form-container';
import { Form } from '@/components/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo, useMemo } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { z } from 'zod';
import { VariableType } from '../../constant';
import { INextOperatorForm } from '../../interface';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { QueryVariable } from '../components/query-variable';
import { DynamicOutput } from './dynamic-output';
import { OutputArray } from './interface';
import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-form-change';
const FormSchema = z.object({
query: z.string().optional(),
outputs: z.array(z.object({ name: z.string(), value: z.any() })).optional(),
});
function IterationForm({ node }: INextOperatorForm) {
const defaultValues = useValues(node);
const form = useForm({
defaultValues: defaultValues,
resolver: zodResolver(FormSchema),
});
const outputs: OutputArray = useWatch({
control: form?.control,
name: 'outputs',
});
const outputList = useMemo(() => {
return outputs.map((x) => ({ title: x.name, type: x?.type }));
}, [outputs]);
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<QueryVariable
name="items_ref"
type={VariableType.Array}
></QueryVariable>
</FormContainer>
<DynamicOutput node={node}></DynamicOutput>
<Output list={outputList}></Output>
</FormWrapper>
</Form>
);
}
export default memo(IterationForm);

View File

@ -0,0 +1,2 @@
export type OutputArray = Array<{ name: string; ref: string; type?: string }>;
export type OutputObject = Record<string, { ref: string; type?: string }>;

View File

@ -0,0 +1,31 @@
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { Operator } from '../../constant';
import { buildOutputOptions } from '../../hooks/use-get-begin-query';
import useGraphStore from '../../store';
export function useBuildSubNodeOutputOptions(nodeId?: string) {
const { nodes } = useGraphStore((state) => state);
const nodeOutputOptions = useMemo(() => {
if (!nodeId) {
return [];
}
const subNodeWithOutputList = nodes.filter(
(x) =>
x.parentId === nodeId &&
x.data.label !== Operator.IterationStart &&
!isEmpty(x.data?.form?.outputs),
);
return subNodeWithOutputList.map((x) => ({
label: x.data.name,
value: x.id,
title: x.data.name,
options: buildOutputOptions(x.data.form.outputs, x.id),
}));
}, [nodeId, nodes]);
return nodeOutputOptions;
}

View File

@ -0,0 +1,27 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { initialIterationValues } from '../../constant';
import { OutputObject } from './interface';
function convertToArray(outputObject: OutputObject) {
return Object.entries(outputObject).map(([key, value]) => ({
name: key,
ref: value.ref,
type: value.type,
}));
}
export function useValues(node?: RAGFlowNodeType) {
const values = useMemo(() => {
const formData = node?.data?.form;
if (isEmpty(formData)) {
return { ...initialIterationValues, outputs: [] };
}
return { ...formData, outputs: convertToArray(formData.outputs) };
}, [node?.data?.form]);
return values;
}

View File

@ -0,0 +1,30 @@
import { useEffect } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import useGraphStore from '../../store';
import { OutputArray, OutputObject } from './interface';
export function transferToObject(list: OutputArray) {
return list.reduce<OutputObject>((pre, cur) => {
pre[cur.name] = { ref: cur.ref, type: cur.type };
return pre;
}, {});
}
export function useWatchFormChange(id?: string, form?: UseFormReturn) {
let values = useWatch({ control: form?.control });
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
useEffect(() => {
// Manually triggered form updates are synchronized to the canvas
if (id && form?.formState.isDirty) {
values = form?.getValues();
console.log('🚀 ~ useEffect ~ values:', values);
let nextValues: any = {
...values,
outputs: transferToObject(values.outputs),
};
updateNodeForm(id, nextValues);
}
}, [form?.formState.isDirty, id, updateNodeForm, values]);
}

View File

@ -0,0 +1,23 @@
import { Output, OutputType } from '@/pages/agent/form/components/output';
import { memo } from 'react';
import { initialIterationStartValues } from '../../constant';
const outputs = initialIterationStartValues.outputs;
const outputList = Object.entries(outputs).reduce<OutputType[]>(
(pre, [key, value]) => {
pre.push({ title: key, type: value.type });
return pre;
},
[],
);
function IterationStartForm() {
return (
<section className="space-y-6 p-4">
<Output list={outputList}></Output>
</section>
);
}
export default memo(IterationStartForm);

View File

@ -0,0 +1,145 @@
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Input, Select } from 'antd';
import { useMemo } from 'react';
import { IOperatorForm } from '../../interface';
import {
Jin10CalendarDatashapeOptions,
Jin10CalendarTypeOptions,
Jin10FlashTypeOptions,
Jin10SymbolsDatatypeOptions,
Jin10SymbolsTypeOptions,
Jin10TypeOptions,
} from '../../options';
import DynamicInputVariable from '../components/dynamic-input-variable';
const Jin10Form = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const jin10TypeOptions = useMemo(() => {
return Jin10TypeOptions.map((x) => ({
value: x,
label: t(`jin10TypeOptions.${x}`),
}));
}, [t]);
const jin10FlashTypeOptions = useMemo(() => {
return Jin10FlashTypeOptions.map((x) => ({
value: x,
label: t(`jin10FlashTypeOptions.${x}`),
}));
}, [t]);
const jin10CalendarTypeOptions = useMemo(() => {
return Jin10CalendarTypeOptions.map((x) => ({
value: x,
label: t(`jin10CalendarTypeOptions.${x}`),
}));
}, [t]);
const jin10CalendarDatashapeOptions = useMemo(() => {
return Jin10CalendarDatashapeOptions.map((x) => ({
value: x,
label: t(`jin10CalendarDatashapeOptions.${x}`),
}));
}, [t]);
const jin10SymbolsTypeOptions = useMemo(() => {
return Jin10SymbolsTypeOptions.map((x) => ({
value: x,
label: t(`jin10SymbolsTypeOptions.${x}`),
}));
}, [t]);
const jin10SymbolsDatatypeOptions = useMemo(() => {
return Jin10SymbolsDatatypeOptions.map((x) => ({
value: x,
label: t(`jin10SymbolsDatatypeOptions.${x}`),
}));
}, [t]);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item label={t('type')} name={'type'} initialValue={'flash'}>
<Select options={jin10TypeOptions}></Select>
</Form.Item>
<Form.Item label={t('secretKey')} name={'secret_key'}>
<Input></Input>
</Form.Item>
<Form.Item noStyle dependencies={['type']}>
{({ getFieldValue }) => {
const type = getFieldValue('type');
switch (type) {
case 'flash':
return (
<>
<Form.Item label={t('flashType')} name={'flash_type'}>
<Select options={jin10FlashTypeOptions}></Select>
</Form.Item>
<Form.Item label={t('contain')} name={'contain'}>
<Input></Input>
</Form.Item>
<Form.Item label={t('filter')} name={'filter'}>
<Input></Input>
</Form.Item>
</>
);
case 'calendar':
return (
<>
<Form.Item label={t('calendarType')} name={'calendar_type'}>
<Select options={jin10CalendarTypeOptions}></Select>
</Form.Item>
<Form.Item
label={t('calendarDatashape')}
name={'calendar_datashape'}
>
<Select options={jin10CalendarDatashapeOptions}></Select>
</Form.Item>
</>
);
case 'symbols':
return (
<>
<Form.Item label={t('symbolsType')} name={'symbols_type'}>
<Select options={jin10SymbolsTypeOptions}></Select>
</Form.Item>
<Form.Item
label={t('symbolsDatatype')}
name={'symbols_datatype'}
>
<Select options={jin10SymbolsDatatypeOptions}></Select>
</Form.Item>
</>
);
case 'news':
return (
<>
<Form.Item label={t('contain')} name={'contain'}>
<Input></Input>
</Form.Item>
<Form.Item label={t('filter')} name={'filter'}>
<Input></Input>
</Form.Item>
</>
);
default:
return <></>;
}
}}
</Form.Item>
</Form>
);
};
export default Jin10Form;

View File

@ -0,0 +1,48 @@
import { NextLLMSelect } from '@/components/llm-select/next';
import { TopNFormField } from '@/components/top-n-item';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { useTranslation } from 'react-i18next';
import { INextOperatorForm } from '../../interface';
import { DynamicInputVariable } from '../components/next-dynamic-input-variable';
const KeywordExtractForm = ({ form, node }: INextOperatorForm) => {
const { t } = useTranslation();
return (
<Form {...form}>
<form
className="space-y-6"
onSubmit={(e) => {
e.preventDefault();
}}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<FormField
control={form.control}
name="llm_id"
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('chat.modelTip')}>
{t('chat.model')}
</FormLabel>
<FormControl>
<NextLLMSelect {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<TopNFormField></TopNFormField>
</form>
</Form>
);
};
export default KeywordExtractForm;

View File

@ -0,0 +1,101 @@
import { FormContainer } from '@/components/form-container';
import { BlockButton, Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { X } from 'lucide-react';
import { memo } from 'react';
import { useFieldArray, useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { INextOperatorForm } from '../../interface';
import { FormWrapper } from '../components/form-wrapper';
import { PromptEditor } from '../components/prompt-editor';
import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-change';
function MessageForm({ node }: INextOperatorForm) {
const { t } = useTranslation();
const values = useValues(node);
const FormSchema = z.object({
content: z
.array(
z.object({
value: z.string(),
}),
)
.optional(),
});
const form = useForm({
defaultValues: values,
resolver: zodResolver(FormSchema),
});
useWatchFormChange(node?.id, form);
const { fields, append, remove } = useFieldArray({
name: 'content',
control: form.control,
});
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<FormItem>
<FormLabel tooltip={t('flow.msgTip')}>{t('flow.msg')}</FormLabel>
<div className="space-y-4">
{fields.map((field, index) => (
<div key={field.id} className="flex items-start gap-2">
<FormField
control={form.control}
name={`content.${index}.value`}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
{/* <Textarea {...field}> </Textarea> */}
<PromptEditor
{...field}
placeholder={t('flow.messagePlaceholder')}
></PromptEditor>
</FormControl>
</FormItem>
)}
/>
{fields.length > 1 && (
<Button
type="button"
variant={'ghost'}
onClick={() => remove(index)}
>
<X />
</Button>
)}
</div>
))}
<BlockButton
type="button"
onClick={() => append({ value: '' })} // "" will cause the inability to add, refer to: https://github.com/orgs/react-hook-form/discussions/8485#discussioncomment-2961861
>
{t('flow.addMessage')}
</BlockButton>
</div>
<FormMessage />
</FormItem>
</FormContainer>
</FormWrapper>
</Form>
);
}
export default memo(MessageForm);

View File

@ -0,0 +1,22 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { initialMessageValues } from '../../constant';
import { convertToObjectArray } from '../../utils';
export function useValues(node?: RAGFlowNodeType) {
const values = useMemo(() => {
const formData = node?.data?.form;
if (isEmpty(formData)) {
return initialMessageValues;
}
return {
...formData,
content: convertToObjectArray(formData.content),
};
}, [node]);
return values;
}

View File

@ -0,0 +1,24 @@
import { useEffect } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import useGraphStore from '../../store';
import { convertToStringArray } from '../../utils';
export function useWatchFormChange(id?: string, form?: UseFormReturn) {
let values = useWatch({ control: form?.control });
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
useEffect(() => {
// Manually triggered form updates are synchronized to the canvas
if (id && form?.formState.isDirty) {
values = form?.getValues();
let nextValues: any = values;
nextValues = {
...values,
content: convertToStringArray(values.content),
};
updateNodeForm(id, nextValues);
}
}, [form?.formState.isDirty, id, updateNodeForm, values]);
}

View File

@ -0,0 +1,90 @@
import { FormContainer } from '@/components/form-container';
import { TopNFormField } from '@/components/top-n-item';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { useTranslate } from '@/hooks/common-hooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo } from 'react';
import { useForm, useFormContext } from 'react-hook-form';
import { z } from 'zod';
import { initialPubMedValues } from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { QueryVariable } from '../components/query-variable';
export const PubMedFormPartialSchema = {
top_n: z.number(),
email: z.string().email(),
};
export const FormSchema = z.object({
...PubMedFormPartialSchema,
query: z.string(),
});
export function PubMedFormWidgets() {
const form = useFormContext();
const { t } = useTranslate('flow');
return (
<>
<TopNFormField></TopNFormField>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('emailTip')}>{t('email')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
);
}
const outputList = buildOutputList(initialPubMedValues.outputs);
function PubMedForm({ node }: INextOperatorForm) {
const defaultValues = useFormValues(initialPubMedValues, node);
const form = useForm<z.infer<typeof FormSchema>>({
defaultValues,
resolver: zodResolver(FormSchema),
mode: 'onChange',
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<QueryVariable></QueryVariable>
</FormContainer>
<FormContainer>
<PubMedFormWidgets></PubMedFormWidgets>
</FormContainer>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
}
export default memo(PubMedForm);

View File

@ -0,0 +1,157 @@
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { RAGFlowSelect } from '@/components/ui/select';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { INextOperatorForm } from '../../interface';
import {
QWeatherLangOptions,
QWeatherTimePeriodOptions,
QWeatherTypeOptions,
QWeatherUserTypeOptions,
} from '../../options';
import { DynamicInputVariable } from '../components/next-dynamic-input-variable';
enum FormFieldName {
Type = 'type',
UserType = 'user_type',
}
const QWeatherForm = ({ form, node }: INextOperatorForm) => {
const { t } = useTranslation();
const typeValue = form.watch(FormFieldName.Type);
const qWeatherLangOptions = useMemo(() => {
return QWeatherLangOptions.map((x) => ({
value: x,
label: t(`flow.qWeatherLangOptions.${x}`),
}));
}, [t]);
const qWeatherTypeOptions = useMemo(() => {
return QWeatherTypeOptions.map((x) => ({
value: x,
label: t(`flow.qWeatherTypeOptions.${x}`),
}));
}, [t]);
const qWeatherUserTypeOptions = useMemo(() => {
return QWeatherUserTypeOptions.map((x) => ({
value: x,
label: t(`flow.qWeatherUserTypeOptions.${x}`),
}));
}, [t]);
const getQWeatherTimePeriodOptions = useCallback(() => {
let options = QWeatherTimePeriodOptions;
const userType = form.getValues(FormFieldName.UserType);
if (userType === 'free') {
options = options.slice(0, 3);
}
return options.map((x) => ({
value: x,
label: t(`flow.qWeatherTimePeriodOptions.${x}`),
}));
}, [form, t]);
return (
<Form {...form}>
<form
className="space-y-6"
onSubmit={(e) => {
e.preventDefault();
}}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<FormField
control={form.control}
name="web_apikey"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.webApiKey')}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lang"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.lang')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={qWeatherLangOptions}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={FormFieldName.Type}
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.type')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={qWeatherTypeOptions}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={FormFieldName.UserType}
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.userType')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={qWeatherUserTypeOptions}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{typeValue === 'weather' && (
<FormField
control={form.control}
name={'time_period'}
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.timePeriod')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={getQWeatherTimePeriodOptions()}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</form>
</Form>
);
};
export default QWeatherForm;

View File

@ -0,0 +1,41 @@
import pick from 'lodash/pick';
import { useCallback, useEffect } from 'react';
import { IOperatorForm } from '../../interface';
import useGraphStore from '../../store';
export const useBuildRelevantOptions = () => {
const nodes = useGraphStore((state) => state.nodes);
const buildRelevantOptions = useCallback(
(toList: string[]) => {
return nodes
.filter(
(x) => !toList.some((y) => y === x.id), // filter out selected values in other to fields from the current drop-down box options
)
.map((x) => ({ label: x.data.name, value: x.id }));
},
[nodes],
);
return buildRelevantOptions;
};
/**
* monitor changes in the connection and synchronize the target to the yes and no fields of the form
* similar to the categorize-form's useHandleFormValuesChange method
* @param param0
*/
export const useWatchConnectionChanges = ({ nodeId, form }: IOperatorForm) => {
const getNode = useGraphStore((state) => state.getNode);
const node = getNode(nodeId);
const watchFormChanges = useCallback(() => {
if (node) {
form?.setFieldsValue(pick(node, ['yes', 'no']));
}
}, [node, form]);
useEffect(() => {
watchFormChanges();
}, [watchFormChanges]);
};

View File

@ -0,0 +1,49 @@
import LLMSelect from '@/components/llm-select';
import { useTranslate } from '@/hooks/common-hooks';
import { Form, Select } from 'antd';
import { Operator } from '../../constant';
import { useBuildFormSelectOptions } from '../../form-hooks';
import { IOperatorForm } from '../../interface';
import { useWatchConnectionChanges } from './hooks';
const RelevantForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const buildRelevantOptions = useBuildFormSelectOptions(
Operator.Relevant,
node?.id,
);
useWatchConnectionChanges({ nodeId: node?.id, form });
return (
<Form
name="basic"
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
onValuesChange={onValuesChange}
autoComplete="off"
form={form}
>
<Form.Item
name={'llm_id'}
label={t('model', { keyPrefix: 'chat' })}
tooltip={t('modelTip', { keyPrefix: 'chat' })}
>
<LLMSelect></LLMSelect>
</Form.Item>
<Form.Item label={t('yes')} name={'yes'}>
<Select
allowClear
options={buildRelevantOptions([form?.getFieldValue('no')])}
/>
</Form.Item>
<Form.Item label={t('no')} name={'no'}>
<Select
allowClear
options={buildRelevantOptions([form?.getFieldValue('yes')])}
/>
</Form.Item>
</Form>
);
};
export default RelevantForm;

View File

@ -0,0 +1,126 @@
import { Collapse } from '@/components/collapse';
import { CrossLanguageFormField } from '@/components/cross-language-form-field';
import { FormContainer } from '@/components/form-container';
import { KnowledgeBaseFormField } from '@/components/knowledge-base-item';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { RerankFormFields } from '@/components/rerank';
import { SimilaritySliderFormField } from '@/components/similarity-slider';
import { TopNFormField } from '@/components/top-n-item';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Textarea } from '@/components/ui/textarea';
import { UseKnowledgeGraphFormField } from '@/components/use-knowledge-graph-item';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo, useMemo } from 'react';
import { useForm, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { initialRetrievalValues } from '../../constant';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { PromptEditor } from '../components/prompt-editor';
import { useValues } from './use-values';
export const RetrievalPartialSchema = {
similarity_threshold: z.coerce.number(),
keywords_similarity_weight: z.coerce.number(),
top_n: z.coerce.number(),
top_k: z.coerce.number(),
kb_ids: z.array(z.string()),
rerank_id: z.string(),
empty_response: z.string(),
cross_languages: z.array(z.string()),
use_kg: z.boolean(),
};
export const FormSchema = z.object({
query: z.string().optional(),
...RetrievalPartialSchema,
});
export function EmptyResponseField() {
const { t } = useTranslation();
const form = useFormContext();
return (
<FormField
control={form.control}
name="empty_response"
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('chat.emptyResponseTip')}>
{t('chat.emptyResponse')}
</FormLabel>
<FormControl>
<Textarea
placeholder={t('common.namePlaceholder')}
{...field}
autoComplete="off"
rows={4}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}
function RetrievalForm({ node }: INextOperatorForm) {
const { t } = useTranslation();
const outputList = useMemo(() => {
return [
{
title: 'formalized_content',
type: initialRetrievalValues.outputs.formalized_content.type,
},
];
}, []);
const defaultValues = useValues(node);
const form = useForm({
defaultValues: defaultValues,
resolver: zodResolver(FormSchema),
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<RAGFlowFormItem name="query" label={t('flow.query')}>
<PromptEditor></PromptEditor>
</RAGFlowFormItem>
<KnowledgeBaseFormField showVariable></KnowledgeBaseFormField>
</FormContainer>
<Collapse title={<div>{t('flow.advancedSettings')}</div>}>
<FormContainer>
<SimilaritySliderFormField
vectorSimilarityWeightName="keywords_similarity_weight"
isTooltipShown
></SimilaritySliderFormField>
<TopNFormField></TopNFormField>
<RerankFormFields></RerankFormFields>
<EmptyResponseField></EmptyResponseField>
<CrossLanguageFormField name="cross_languages"></CrossLanguageFormField>
<UseKnowledgeGraphFormField name="use_kg"></UseKnowledgeGraphFormField>
</FormContainer>
</Collapse>
<Output list={outputList}></Output>
</FormWrapper>
</Form>
);
}
export default memo(RetrievalForm);

View File

@ -0,0 +1,25 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { initialRetrievalValues } from '../../constant';
export function useValues(node?: RAGFlowNodeType) {
const defaultValues = useMemo(
() => ({
...initialRetrievalValues,
}),
[],
);
const values = useMemo(() => {
const formData = node?.data?.form;
if (isEmpty(formData)) {
return defaultValues;
}
return formData;
}, [defaultValues, node?.data?.form]);
return values;
}

View File

@ -0,0 +1,68 @@
import { NextLLMSelect } from '@/components/llm-select/next';
import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { RAGFlowSelect } from '@/components/ui/select';
import { useTranslation } from 'react-i18next';
import { INextOperatorForm } from '../../interface';
import { GoogleLanguageOptions } from '../../options';
const RewriteQuestionForm = ({ form }: INextOperatorForm) => {
const { t } = useTranslation();
return (
<Form {...form}>
<form
className="space-y-6"
onSubmit={(e) => {
e.preventDefault();
}}
>
<FormField
control={form.control}
name="llm_id"
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('chat.modelTip')}>
{t('chat.model')}
</FormLabel>
<FormControl>
<NextLLMSelect {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="language"
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('chat.languageTip')}>
{t('chat.language')}
</FormLabel>
<FormControl>
<RAGFlowSelect
options={GoogleLanguageOptions}
allowClear={true}
{...field}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField>
</form>
</Form>
);
};
export default RewriteQuestionForm;

View File

@ -0,0 +1,73 @@
import { FormContainer } from '@/components/form-container';
import { TopNFormField } from '@/components/top-n-item';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { useTranslate } from '@/hooks/common-hooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { initialSearXNGValues } from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { QueryVariable } from '../components/query-variable';
const FormSchema = z.object({
query: z.string(),
searxng_url: z.string().min(1),
top_n: z.string(),
});
const outputList = buildOutputList(initialSearXNGValues.outputs);
function SearXNGForm({ node }: INextOperatorForm) {
const { t } = useTranslate('flow');
const defaultValues = useFormValues(initialSearXNGValues, node);
const form = useForm<z.infer<typeof FormSchema>>({
defaultValues,
resolver: zodResolver(FormSchema),
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<QueryVariable></QueryVariable>
<TopNFormField></TopNFormField>
<FormField
control={form.control}
name="searxng_url"
render={({ field }) => (
<FormItem>
<FormLabel>SearXNG URL</FormLabel>
<FormControl>
<Input {...field} placeholder="http://localhost:4000" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</FormContainer>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
}
export default memo(SearXNGForm);

View File

@ -0,0 +1,166 @@
import { FormContainer } from '@/components/form-container';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { MultiSelect } from '@/components/ui/multi-select';
import { RAGFlowSelect } from '@/components/ui/select';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from 'i18next';
import { toLower } from 'lodash';
import { memo, useCallback, useMemo } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { z } from 'zod';
import {
StringTransformDelimiter,
StringTransformMethod,
initialStringTransformValues,
} from '../../constant';
import { INextOperatorForm } from '../../interface';
import { FormWrapper } from '../components/form-wrapper';
import { Output, transferOutputs } from '../components/output';
import { PromptEditor } from '../components/prompt-editor';
import { QueryVariable } from '../components/query-variable';
import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-form-change';
const DelimiterOptions = Object.entries(StringTransformDelimiter).map(
([key, val]) => ({ label: t('flow.' + toLower(key)), value: val }),
);
function StringTransformForm({ node }: INextOperatorForm) {
const values = useValues(node);
const FormSchema = z.object({
method: z.string(),
split_ref: z.string().optional(),
script: z.string().optional(),
delimiters: z.array(z.string()).or(z.string()),
outputs: z.object({ result: z.object({ type: z.string() }) }).optional(),
});
const form = useForm<z.infer<typeof FormSchema>>({
defaultValues: values,
resolver: zodResolver(FormSchema),
});
const method = useWatch({ control: form.control, name: 'method' });
const isSplit = method === StringTransformMethod.Split;
const outputList = useMemo(() => {
return transferOutputs(values.outputs);
}, [values.outputs]);
const handleMethodChange = useCallback(
(value: StringTransformMethod) => {
const isMerge = value === StringTransformMethod.Merge;
const outputs = {
...initialStringTransformValues.outputs,
result: {
type: isMerge ? 'string' : 'Array<string>',
},
};
form.setValue('outputs', outputs);
form.setValue(
'delimiters',
isMerge ? StringTransformDelimiter.Comma : [],
);
},
[form],
);
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<FormField
control={form.control}
name="method"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.method')}</FormLabel>
<FormControl>
<RAGFlowSelect
{...field}
options={Object.values(StringTransformMethod).map(
(val) => ({ label: t('flow.' + val), value: val }),
)}
onChange={(value) => {
handleMethodChange(value);
field.onChange(value);
}}
></RAGFlowSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{isSplit && (
<QueryVariable
label={<FormLabel>split_ref</FormLabel>}
name="split_ref"
></QueryVariable>
)}
{isSplit || (
<FormField
control={form.control}
name="script"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.script')}</FormLabel>
<FormControl>
<PromptEditor {...field} showToolbar={false}></PromptEditor>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="delimiters"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.delimiters')}</FormLabel>
<FormControl>
{isSplit ? (
<MultiSelect
options={DelimiterOptions}
onValueChange={field.onChange}
defaultValue={field.value as string[]}
variant="inverted"
// {...field}
/>
) : (
<RAGFlowSelect
{...field}
options={DelimiterOptions}
></RAGFlowSelect>
)}
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="outputs"
render={() => <div></div>}
/>
</FormContainer>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
}
export default memo(StringTransformForm);

View File

@ -0,0 +1,33 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import {
initialStringTransformValues,
StringTransformMethod,
} from '../../constant';
function transferDelimiters(formData: typeof initialStringTransformValues) {
return formData.method === StringTransformMethod.Merge
? formData.delimiters[0]
: formData.delimiters;
}
export function useValues(node?: RAGFlowNodeType) {
const values = useMemo(() => {
const formData = node?.data?.form;
if (isEmpty(formData)) {
return {
...initialStringTransformValues,
delimiters: transferDelimiters(formData),
};
}
return {
...formData,
delimiters: transferDelimiters(formData),
};
}, [node?.data?.form]);
return values;
}

View File

@ -0,0 +1,26 @@
import { useEffect } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import { StringTransformMethod } from '../../constant';
import useGraphStore from '../../store';
export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) {
let values = useWatch({ control: form?.control });
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
useEffect(() => {
// Manually triggered form updates are synchronized to the canvas
if (id && form?.formState.isDirty) {
values = form?.getValues();
let nextValues: any = values;
if (
values.delimiters !== undefined &&
values.method === StringTransformMethod.Merge
) {
nextValues.delimiters = [values.delimiters];
}
updateNodeForm(id, nextValues);
}
}, [form?.formState.isDirty, id, updateNodeForm, values]);
}

View File

@ -0,0 +1,328 @@
import { FormContainer } from '@/components/form-container';
import { IconFont } from '@/components/icon-font';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { BlockButton, Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from '@/components/ui/form';
import { RAGFlowSelect } from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from 'i18next';
import { toLower } from 'lodash';
import { X } from 'lucide-react';
import { memo, useCallback, useMemo } from 'react';
import { useFieldArray, useForm, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import {
SwitchLogicOperatorOptions,
SwitchOperatorOptions,
VariableType,
} from '../../constant';
import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query';
import { IOperatorForm } from '../../interface';
import { FormWrapper } from '../components/form-wrapper';
import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-change';
const ConditionKey = 'conditions';
const ItemKey = 'items';
type ConditionCardsProps = {
name: string;
removeParent(index: number): void;
parentIndex: number;
parentLength: number;
} & IOperatorForm;
export const LogicalOperatorIcon = function OperatorIcon({
icon,
value,
}: Omit<(typeof SwitchOperatorOptions)[0], 'label'>) {
if (typeof icon === 'string') {
return (
<IconFont
name={icon}
className={cn('size-4', {
'rotate-180': value === '>',
})}
></IconFont>
);
}
return icon;
};
export function useBuildSwitchOperatorOptions() {
const { t } = useTranslation();
const switchOperatorOptions = useMemo(() => {
return SwitchOperatorOptions.map((x) => ({
value: x.value,
icon: (
<LogicalOperatorIcon
icon={x.icon}
value={x.value}
></LogicalOperatorIcon>
),
label: t(`flow.switchOperatorOptions.${x.label}`),
}));
}, [t]);
return switchOperatorOptions;
}
function ConditionCards({
name: parentName,
parentIndex,
removeParent,
parentLength,
}: ConditionCardsProps) {
const form = useFormContext();
const nextOptions = useBuildQueryVariableOptions();
const finalOptions = useMemo(() => {
return nextOptions.map((x) => {
return {
...x,
options: x.options.filter(
(y) => !toLower(y.type).includes(VariableType.Array),
),
};
});
}, [nextOptions]);
const switchOperatorOptions = useBuildSwitchOperatorOptions();
const name = `${parentName}.${ItemKey}`;
const { fields, remove, append } = useFieldArray({
name: name,
control: form.control,
});
const handleRemove = useCallback(
(index: number) => () => {
remove(index);
if (parentIndex !== 0 && index === 0 && parentLength === 1) {
removeParent(parentIndex);
}
},
[parentIndex, parentLength, remove, removeParent],
);
return (
<section className="flex-1 space-y-2.5 min-w-0">
{fields.map((field, index) => {
return (
<div key={field.id} className="flex">
<Card
className={cn(
'relative bg-transparent border-input-border border flex-1 min-w-0',
{
'before:w-10 before:absolute before:h-[1px] before:bg-input-border before:top-1/2 before:-left-10':
fields.length > 1 &&
(index === 0 || index === fields.length - 1),
},
)}
>
<section className="p-2 bg-bg-card flex justify-between items-center">
<FormField
control={form.control}
name={`${name}.${index}.cpn_id`}
render={({ field }) => (
<FormItem className="flex-1 min-w-0">
<FormControl>
<SelectWithSearch
{...field}
options={finalOptions}
triggerClassName="text-accent-primary bg-transparent border-none truncate"
></SelectWithSearch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-center">
<Separator orientation="vertical" className="h-2.5" />
<FormField
control={form.control}
name={`${name}.${index}.operator`}
render={({ field }) => (
<FormItem>
<FormControl>
<RAGFlowSelect
{...field}
options={switchOperatorOptions}
onlyShowSelectedIcon
triggerClassName="w-30 bg-transparent border-none"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</section>
<CardContent className="p-4 ">
<FormField
control={form.control}
name={`${name}.${index}.value`}
render={({ field }) => (
<FormItem>
<FormControl>
<Textarea {...field} className="bg-transparent" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
<Button variant={'ghost'} onClick={handleRemove(index)}>
<X />
</Button>
</div>
);
})}
<div className="pr-9">
<BlockButton
className="mt-6"
onClick={() => append({ operator: switchOperatorOptions[0].value })}
>
{t('common.add')}
</BlockButton>
</div>
</section>
);
}
function SwitchForm({ node }: IOperatorForm) {
const { t } = useTranslation();
const values = useValues(node);
const switchOperatorOptions = useBuildSwitchOperatorOptions();
const FormSchema = z.object({
conditions: z.array(
z
.object({
logical_operator: z.string(),
items: z
.array(
z.object({
cpn_id: z.string(),
operator: z.string(),
value: z.string().optional(),
}),
)
.optional(),
to: z.array(z.string()).optional(),
})
.optional(),
),
});
const form = useForm({
defaultValues: values,
resolver: zodResolver(FormSchema),
});
const { fields, remove, append } = useFieldArray({
name: ConditionKey,
control: form.control,
});
const switchLogicOperatorOptions = useMemo(() => {
return SwitchLogicOperatorOptions.map((x) => ({
value: x,
label: t(`flow.switchLogicOperatorOptions.${x}`),
}));
}, [t]);
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
{fields.map((field, index) => {
const name = `${ConditionKey}.${index}`;
const conditions: Array<any> = form.getValues(`${name}.${ItemKey}`);
const conditionLength = conditions.length;
return (
<FormContainer key={field.id} className="">
<div className="flex justify-between items-center">
<section>
<span>{index === 0 ? 'IF' : 'ELSEIF'}</span>
<div className="text-text-secondary">Case {index + 1}</div>
</section>
{index !== 0 && (
<Button
variant={'secondary'}
className="-translate-y-1"
onClick={() => remove(index)}
>
{t('common.remove')} <X />
</Button>
)}
</div>
<section className="flex gap-2 !mt-2 relative">
{conditionLength > 1 && (
<section className="flex flex-col w-[72px]">
<div className="relative w-1 flex-1 before:absolute before:w-[1px] before:bg-input-border before:top-20 before:bottom-0 before:left-10"></div>
<FormField
control={form.control}
name={`${ConditionKey}.${index}.logical_operator`}
render={({ field }) => (
<FormItem>
<FormControl>
<RAGFlowSelect
{...field}
options={switchLogicOperatorOptions}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="relative w-1 flex-1 before:absolute before:w-[1px] before:bg-input-border before:top-0 before:bottom-36 before:left-10"></div>
</section>
)}
<ConditionCards
name={name}
removeParent={remove}
parentIndex={index}
parentLength={fields.length}
></ConditionCards>
</section>
</FormContainer>
);
})}
<BlockButton
onClick={() =>
append({
logical_operator: SwitchLogicOperatorOptions[0],
[ItemKey]: [
{
operator: switchOperatorOptions[0].value,
},
],
to: [],
})
}
>
{t('common.add')}
</BlockButton>
</FormWrapper>
</Form>
);
}
export default memo(SwitchForm);

View File

@ -0,0 +1,17 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { initialSwitchValues } from '../../constant';
export function useValues(node?: RAGFlowNodeType) {
const values = useMemo(() => {
const formData = node?.data?.form;
if (isEmpty(formData)) {
return initialSwitchValues;
}
return formData;
}, [node]);
return values;
}

View File

@ -0,0 +1,24 @@
import { ISwitchCondition } from '@/interfaces/database/agent';
import { useEffect } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import useGraphStore from '../../store';
export function useWatchFormChange(id?: string, form?: UseFormReturn) {
let values = useWatch({ control: form?.control });
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
useEffect(() => {
// Manually triggered form updates are synchronized to the canvas
console.log('🚀 ~ useWatchFormChange ~ values:', form?.formState.isDirty);
if (id) {
values = form?.getValues() || {};
let nextValues: any = {
...values,
conditions:
values?.conditions?.map((x: ISwitchCondition) => ({ ...x })) ?? [], // Changing the form value with useFieldArray does not change the array reference
};
updateNodeForm(id, nextValues);
}
}, [form?.formState.isDirty, id, updateNodeForm, values]);
}

View File

@ -0,0 +1,121 @@
import { FormContainer } from '@/components/form-container';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { RAGFlowSelect } from '@/components/ui/select';
import { buildOptions } from '@/utils/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from 'i18next';
import { memo } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import {
TavilyExtractDepth,
TavilyExtractFormat,
initialTavilyExtractValues,
} from '../../constant';
import { useFormValues } from '../../hooks/use-form-values';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { ApiKeyField } from '../components/api-key-field';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { PromptEditor } from '../components/prompt-editor';
import { TavilyFormSchema } from '../tavily-form';
const outputList = buildOutputList(initialTavilyExtractValues.outputs);
function TavilyExtractForm({ node }: INextOperatorForm) {
const values = useFormValues(initialTavilyExtractValues, node);
const FormSchema = z.object({
...TavilyFormSchema,
urls: z.string(),
extract_depth: z.enum([
TavilyExtractDepth.Advanced,
TavilyExtractDepth.Basic,
]),
format: z.enum([TavilyExtractFormat.Text, TavilyExtractFormat.Markdown]),
});
const form = useForm<z.infer<typeof FormSchema>>({
defaultValues: values,
resolver: zodResolver(FormSchema),
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<ApiKeyField></ApiKeyField>
</FormContainer>
<FormContainer>
<FormField
control={form.control}
name="urls"
render={({ field }) => (
<FormItem>
<FormLabel>URL</FormLabel>
<FormControl>
<PromptEditor
{...field}
multiLine={false}
showToolbar={false}
></PromptEditor>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="extract_depth"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.extractDepth')}</FormLabel>
<FormControl>
<RAGFlowSelect
placeholder="shadcn"
{...field}
options={buildOptions(TavilyExtractDepth, t, 'flow')}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="format"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.format')}</FormLabel>
<FormControl>
<RAGFlowSelect
placeholder="shadcn"
{...field}
options={buildOptions(TavilyExtractFormat)}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</FormContainer>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
}
export default memo(TavilyExtractForm);

View File

@ -0,0 +1,60 @@
import { BlockButton, Button } from '@/components/ui/button';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { t } from 'i18next';
import { X } from 'lucide-react';
import { ReactNode } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
type DynamicDomainProps = { name: string; label: ReactNode };
export const DynamicDomain = ({ name, label }: DynamicDomainProps) => {
const form = useFormContext();
const { fields, append, remove } = useFieldArray({
name: name,
control: form.control,
});
return (
<FormItem>
<FormLabel>{label}</FormLabel>
<div className="space-y-4">
{fields.map((field, index) => (
<div key={field.id} className="flex">
<div className="space-y-2 flex-1">
<FormField
control={form.control}
name={`${name}.${index}.value`}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Input {...field}></Input>
</FormControl>
</FormItem>
)}
/>
</div>
<Button
type="button"
variant={'ghost'}
onClick={() => remove(index)}
>
<X />
</Button>
</div>
))}
</div>
<FormMessage />
<BlockButton onClick={() => append({ value: '' })}>
{t('common.add')}
</BlockButton>
</FormItem>
);
};

View File

@ -0,0 +1,214 @@
import { FormContainer } from '@/components/form-container';
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 { buildOptions } from '@/utils/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { t } from 'i18next';
import { memo } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import {
TavilySearchDepth,
TavilyTopic,
initialTavilyValues,
} from '../../constant';
import { INextOperatorForm } from '../../interface';
import { buildOutputList } from '../../utils/build-output-list';
import { ApiKeyField } from '../components/api-key-field';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { QueryVariable } from '../components/query-variable';
import { DynamicDomain } from './dynamic-domain';
import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-change';
export const TavilyFormSchema = {
api_key: z.string(),
};
const outputList = buildOutputList(initialTavilyValues.outputs);
function TavilyForm({ node }: INextOperatorForm) {
const values = useValues(node);
const FormSchema = z.object({
...TavilyFormSchema,
query: z.string(),
search_depth: z.enum([TavilySearchDepth.Advanced, TavilySearchDepth.Basic]),
topic: z.enum([TavilyTopic.News, TavilyTopic.General]),
max_results: z.coerce.number(),
days: z.coerce.number(),
include_answer: z.boolean(),
include_raw_content: z.boolean(),
include_images: z.boolean(),
include_image_descriptions: z.boolean(),
include_domains: z.array(z.object({ value: z.any() })), // TODO: z.string should be used, but an error will be reported
exclude_domains: z.array(z.object({ value: z.any() })),
});
const form = useForm<z.infer<typeof FormSchema>>({
defaultValues: values,
resolver: zodResolver(FormSchema),
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<FormWrapper>
<FormContainer>
<ApiKeyField></ApiKeyField>
</FormContainer>
<FormContainer>
<QueryVariable></QueryVariable>
<FormField
control={form.control}
name="search_depth"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.searchDepth')}</FormLabel>
<FormControl>
<RAGFlowSelect
placeholder="shadcn"
{...field}
options={buildOptions(TavilySearchDepth, t, 'flow')}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="topic"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.tavilyTopic')}</FormLabel>
<FormControl>
<RAGFlowSelect
placeholder="shadcn"
{...field}
options={buildOptions(TavilyTopic, t, 'flow')}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="max_results"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.maxResults')}</FormLabel>
<FormControl>
<Input type={'number'} {...field}></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="days"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.days')}</FormLabel>
<FormControl>
<Input type={'number'} {...field}></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="include_answer"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.includeAnswer')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
></Switch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="include_raw_content"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.includeRawContent')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
></Switch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="include_images"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.includeImages')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
></Switch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="include_image_descriptions"
render={({ field }) => (
<FormItem>
<FormLabel>{t('flow.includeImageDescriptions')}</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
></Switch>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DynamicDomain
name="include_domains"
label={t('flow.includeDomains')}
></DynamicDomain>
<DynamicDomain
name="exclude_domains"
label={t('flow.ExcludeDomains')}
></DynamicDomain>
</FormContainer>
</FormWrapper>
<div className="p-5">
<Output list={outputList}></Output>
</div>
</Form>
);
}
export default memo(TavilyForm);

View File

@ -0,0 +1,23 @@
import { RAGFlowNodeType } from '@/interfaces/database/agent';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { initialTavilyValues } from '../../constant';
import { convertToObjectArray } from '../../utils';
export function useValues(node?: RAGFlowNodeType) {
const values = useMemo(() => {
const formData = node?.data?.form;
if (isEmpty(formData)) {
return initialTavilyValues;
}
return {
...formData,
include_domains: convertToObjectArray(formData.include_domains),
exclude_domains: convertToObjectArray(formData.exclude_domains),
};
}, [node?.data?.form]);
return values;
}

View File

@ -0,0 +1,23 @@
import { useEffect } from 'react';
import { UseFormReturn, useWatch } from 'react-hook-form';
import useGraphStore from '../../store';
import { convertToStringArray } from '../../utils';
export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) {
let values = useWatch({ control: form?.control });
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
useEffect(() => {
// Manually triggered form updates are synchronized to the canvas
if (id) {
values = form?.getValues();
let nextValues: any = {
...values,
include_domains: convertToStringArray(values.include_domains),
exclude_domains: convertToStringArray(values.exclude_domains),
};
updateNodeForm(id, nextValues);
}
}, [form?.formState.isDirty, id, updateNodeForm, values]);
}

View File

@ -0,0 +1,83 @@
import { useTranslate } from '@/hooks/common-hooks';
import { DatePicker, DatePickerProps, Form, Input, Select } from 'antd';
import dayjs from 'dayjs';
import { useCallback, useMemo } from 'react';
import { IOperatorForm } from '../../interface';
import { TuShareSrcOptions } from '../../options';
import DynamicInputVariable from '../components/dynamic-input-variable';
const DateTimePicker = ({
onChange,
value,
}: {
onChange?: (val: number | undefined) => void;
value?: number | undefined;
}) => {
const handleChange: DatePickerProps['onChange'] = useCallback(
(val: any) => {
const nextVal = val?.format('YYYY-MM-DD HH:mm:ss');
onChange?.(nextVal ? nextVal : undefined);
},
[onChange],
);
// The value needs to be converted into a string and saved to the backend
const nextValue = useMemo(() => {
if (value) {
return dayjs(value);
}
return undefined;
}, [value]);
return (
<DatePicker
showTime
format="YYYY-MM-DD HH:mm:ss"
onChange={handleChange}
value={nextValue}
/>
);
};
const TuShareForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslate('flow');
const tuShareSrcOptions = useMemo(() => {
return TuShareSrcOptions.map((x) => ({
value: x,
label: t(`tuShareSrcOptions.${x}`),
}));
}, [t]);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item
label={t('token')}
name={'token'}
tooltip={'Get from https://tushare.pro/'}
>
<Input></Input>
</Form.Item>
<Form.Item label={t('src')} name={'src'}>
<Select options={tuShareSrcOptions}></Select>
</Form.Item>
<Form.Item label={t('startDate')} name={'start_date'}>
<DateTimePicker />
</Form.Item>
<Form.Item label={t('endDate')} name={'end_date'}>
<DateTimePicker />
</Form.Item>
<Form.Item label={t('keyword')} name={'keyword'}>
<Input></Input>
</Form.Item>
</Form>
);
};
export default TuShareForm;

View File

@ -0,0 +1,168 @@
import { Collapse } from '@/components/collapse';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { FormTooltip } from '@/components/ui/tooltip';
import { zodResolver } from '@hookform/resolvers/zod';
import { Plus } from 'lucide-react';
import { memo } from 'react';
import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { BeginQuery, INextOperatorForm } from '../../interface';
import { ParameterDialog } from '../begin-form/parameter-dialog';
import { QueryTable } from '../begin-form/query-table';
import { useEditQueryRecord } from '../begin-form/use-edit-query';
import { Output } from '../components/output';
import { useValues } from './use-values';
import { useWatchFormChange } from './use-watch-change';
function UserFillUpForm({ node }: INextOperatorForm) {
const { t } = useTranslation();
const values = useValues(node);
const FormSchema = z.object({
enable_tips: z.boolean().optional(),
tips: z.string().trim().optional(),
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),
});
useWatchFormChange(node?.id, form);
const inputs: BeginQuery[] = useWatch({
control: form.control,
name: 'inputs',
});
const outputList = inputs?.map((item) => ({
title: item.name,
type: item.type,
}));
const {
ok,
currentRecord,
visible,
hideModal,
showModal,
otherThanCurrentQuery,
handleDeleteRecord,
} = useEditQueryRecord({
form,
node,
});
return (
<section className="px-5 space-y-5">
<Form {...form}>
<FormField
control={form.control}
name={'enable_tips'}
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('flow.openingSwitchTip')}>
{t('flow.guidingQuestion')}
</FormLabel>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={'tips'}
render={({ field }) => (
<FormItem>
<FormLabel tooltip={t('chat.setAnOpenerTip')}>
{t('flow.msg')}
</FormLabel>
<FormControl>
<Textarea
rows={5}
{...field}
placeholder={t('common.pleaseInput')}
></Textarea>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* 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>
}
>
<QueryTable
data={inputs}
showModal={showModal}
deleteRecord={handleDeleteRecord}
></QueryTable>
</Collapse>
{visible && (
<ParameterDialog
hideModal={hideModal}
initialValue={currentRecord}
otherThanCurrentQuery={otherThanCurrentQuery}
submit={ok}
></ParameterDialog>
)}
</Form>
<Output list={outputList}></Output>
</section>
);
}
export default memo(UserFillUpForm);

View File

@ -0,0 +1,21 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { initialUserFillUpValues } from '../../constant';
import { buildBeginInputListFromObject } from '../begin-form/utils';
export function useValues(node?: RAGFlowNodeType) {
const values = useMemo(() => {
const formData = node?.data?.form;
if (isEmpty(formData)) {
return initialUserFillUpValues;
}
const inputs = buildBeginInputListFromObject(formData?.inputs);
return { ...(formData || {}), inputs };
}, [node?.data?.form]);
return values;
}

Some files were not shown because too many files have changed in this diff Show More