mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
Feat: Use data pipeline to visualize the parsing configuration of the knowledge base (#10423)
### What problem does this PR solve? #9869 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --------- Signed-off-by: dependabot[bot] <support@github.com> Signed-off-by: jinhai <haijin.chn@gmail.com> Signed-off-by: Jin Hai <haijin.chn@gmail.com> Co-authored-by: chanx <1243304602@qq.com> Co-authored-by: balibabu <cike8899@users.noreply.github.com> Co-authored-by: Lynn <lynn_inf@hotmail.com> Co-authored-by: 纷繁下的无奈 <zhileihuang@126.com> Co-authored-by: huangzl <huangzl@shinemo.com> Co-authored-by: writinwaters <93570324+writinwaters@users.noreply.github.com> Co-authored-by: Wilmer <33392318@qq.com> Co-authored-by: Adrian Weidig <adrianweidig@gmx.net> Co-authored-by: Zhichang Yu <yuzhichang@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Yongteng Lei <yongtengrey@outlook.com> Co-authored-by: Liu An <asiro@qq.com> Co-authored-by: buua436 <66937541+buua436@users.noreply.github.com> Co-authored-by: BadwomanCraZY <511528396@qq.com> Co-authored-by: cucusenok <31804608+cucusenok@users.noreply.github.com> Co-authored-by: Russell Valentine <russ@coldstonelabs.org> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Billy Bao <newyorkupperbay@gmail.com> Co-authored-by: Zhedong Cen <cenzhedong2@126.com> Co-authored-by: TensorNull <129579691+TensorNull@users.noreply.github.com> Co-authored-by: TensorNull <tensor.null@gmail.com> Co-authored-by: TeslaZY <TeslaZY@outlook.com> Co-authored-by: Ajay <160579663+aybanda@users.noreply.github.com> Co-authored-by: AB <aj@Ajays-MacBook-Air.local> Co-authored-by: 天海蒼灆 <huangaoqin@tecpie.com> Co-authored-by: He Wang <wanghechn@qq.com> Co-authored-by: Atsushi Hatakeyama <atu729@icloud.com> Co-authored-by: Jin Hai <haijin.chn@gmail.com> Co-authored-by: Mohamed Mathari <155896313+melmathari@users.noreply.github.com> Co-authored-by: Mohamed Mathari <nocodeventure@Mac-mini-van-Mohamed.fritz.box> Co-authored-by: Stephen Hu <stephenhu@seismic.com> Co-authored-by: Shaun Zhang <zhangwfjh@users.noreply.github.com> Co-authored-by: zhimeng123 <60221886+zhimeng123@users.noreply.github.com> Co-authored-by: mxc <mxc@example.com> Co-authored-by: Dominik Novotný <50611433+SgtMarmite@users.noreply.github.com> Co-authored-by: EVGENY M <168018528+rjohny55@users.noreply.github.com> Co-authored-by: mcoder6425 <mcoder64@gmail.com> Co-authored-by: lemsn <lemsn@msn.com> Co-authored-by: lemsn <lemsn@126.com> Co-authored-by: Adrian Gora <47756404+adagora@users.noreply.github.com> Co-authored-by: Womsxd <45663319+Womsxd@users.noreply.github.com> Co-authored-by: FatMii <39074672+FatMii@users.noreply.github.com>
This commit is contained in:
@ -43,7 +43,9 @@ function InnerButtonEdge({
|
||||
targetPosition,
|
||||
});
|
||||
const selectedStyle = useMemo(() => {
|
||||
return selected ? { strokeWidth: 1, stroke: 'var(--accent-primary)' } : {};
|
||||
return selected
|
||||
? { strokeWidth: 1, stroke: 'rgb(var(--accent-primary))' }
|
||||
: {};
|
||||
}, [selected]);
|
||||
|
||||
const placeholderHighlightStyle = useMemo(() => {
|
||||
@ -67,7 +69,7 @@ function InnerButtonEdge({
|
||||
let index = idx - 1;
|
||||
while (index >= 0) {
|
||||
if (path[index] === source) {
|
||||
return { strokeWidth: 1, stroke: 'var(--accent-primary)' };
|
||||
return { strokeWidth: 1, stroke: 'rgb(var(--accent-primary))' };
|
||||
}
|
||||
index--;
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
AgentGlobalsSysQueryWithBrace,
|
||||
CodeTemplateStrMap,
|
||||
ProgrammingLanguage,
|
||||
initialLlmBaseValues,
|
||||
} from '@/constants/agent';
|
||||
|
||||
export enum AgentDialogueMode {
|
||||
@ -14,13 +15,8 @@ export enum AgentDialogueMode {
|
||||
Task = 'task',
|
||||
}
|
||||
|
||||
import {
|
||||
ChatVariableEnabledField,
|
||||
variableEnabledFieldMap,
|
||||
} from '@/constants/chat';
|
||||
import { ModelVariableType } from '@/constants/knowledge';
|
||||
import i18n from '@/locales/config';
|
||||
import { setInitialChatVariableEnabledFieldValue } from '@/utils/chat';
|
||||
import { t } from 'i18next';
|
||||
|
||||
// DuckDuckGo's channel options
|
||||
@ -276,24 +272,6 @@ export const initialBeginValues = {
|
||||
prologue: `Hi! I'm your assistant. What can I do for you?`,
|
||||
};
|
||||
|
||||
export const variableCheckBoxFieldMap = Object.keys(
|
||||
variableEnabledFieldMap,
|
||||
).reduce<Record<string, boolean>>((pre, cur) => {
|
||||
pre[cur] = setInitialChatVariableEnabledFieldValue(
|
||||
cur as ChatVariableEnabledField,
|
||||
);
|
||||
return pre;
|
||||
}, {});
|
||||
|
||||
const initialLlmBaseValues = {
|
||||
...variableCheckBoxFieldMap,
|
||||
temperature: 0.1,
|
||||
top_p: 0.3,
|
||||
frequency_penalty: 0.7,
|
||||
presence_penalty: 0.4,
|
||||
max_tokens: 256,
|
||||
};
|
||||
|
||||
export const initialGenerateValues = {
|
||||
...initialLlmBaseValues,
|
||||
prompt: i18n.t('flow.promptText'),
|
||||
|
||||
@ -86,7 +86,7 @@ export function FileUploadDirectUpload({
|
||||
</div>
|
||||
<p className="font-medium text-sm">Drag & drop files here</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
Or click to browse (max 2 files)
|
||||
Or click to browse (max 1 files)
|
||||
</p>
|
||||
</div>
|
||||
<FileUploadTrigger asChild>
|
||||
|
||||
@ -55,7 +55,7 @@ type IProps = {
|
||||
onChange?: (value?: string) => void;
|
||||
placeholder?: ReactNode;
|
||||
} & PromptContentProps &
|
||||
Pick<VariablePickerMenuPluginProps, 'extraOptions'>;
|
||||
Pick<VariablePickerMenuPluginProps, 'extraOptions' | 'baseOptions'>;
|
||||
|
||||
function PromptContent({
|
||||
showToolbar = true,
|
||||
@ -126,6 +126,7 @@ export function PromptEditor({
|
||||
showToolbar,
|
||||
multiLine = true,
|
||||
extraOptions,
|
||||
baseOptions,
|
||||
}: IProps) {
|
||||
const { t } = useTranslation();
|
||||
const initialConfig: InitialConfigType = {
|
||||
@ -177,6 +178,7 @@ export function PromptEditor({
|
||||
<VariablePickerMenuPlugin
|
||||
value={value}
|
||||
extraOptions={extraOptions}
|
||||
baseOptions={baseOptions}
|
||||
></VariablePickerMenuPlugin>
|
||||
<PasteHandlerPlugin />
|
||||
<VariableOnChangePlugin
|
||||
|
||||
@ -10,7 +10,6 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
|
||||
import {
|
||||
LexicalTypeaheadMenuPlugin,
|
||||
MenuOption,
|
||||
useBasicTypeaheadTriggerMatch,
|
||||
} from '@lexical/react/LexicalTypeaheadMenuPlugin';
|
||||
import {
|
||||
$createParagraphNode,
|
||||
@ -109,29 +108,56 @@ function VariablePickerMenuItem({
|
||||
);
|
||||
}
|
||||
|
||||
export type VariablePickerMenuOptionType = {
|
||||
label: string;
|
||||
title: string;
|
||||
value?: string;
|
||||
options: Array<{
|
||||
label: string;
|
||||
value: string;
|
||||
icon: ReactNode;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type VariablePickerMenuPluginProps = {
|
||||
value?: string;
|
||||
extraOptions?: Array<{
|
||||
label: string;
|
||||
title: string;
|
||||
options: Array<{ label: string; value: string; icon?: ReactNode }>;
|
||||
}>;
|
||||
extraOptions?: VariablePickerMenuOptionType[];
|
||||
baseOptions?: VariablePickerMenuOptionType[];
|
||||
};
|
||||
export default function VariablePickerMenuPlugin({
|
||||
value,
|
||||
extraOptions,
|
||||
baseOptions,
|
||||
}: VariablePickerMenuPluginProps): JSX.Element {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const isFirstRender = useRef(true);
|
||||
|
||||
const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
|
||||
minLength: 0,
|
||||
});
|
||||
// const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
|
||||
// minLength: 0,
|
||||
// });
|
||||
|
||||
const testTriggerFn = React.useCallback((text: string) => {
|
||||
const lastChar = text.slice(-1);
|
||||
if (lastChar === '/') {
|
||||
console.log('Found trigger character "/"');
|
||||
return {
|
||||
leadOffset: text.length - 1,
|
||||
matchingString: '',
|
||||
replaceableString: '/',
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
const previousValue = useRef<string | undefined>();
|
||||
|
||||
const [queryString, setQueryString] = React.useState<string | null>('');
|
||||
|
||||
let options = useBuildQueryVariableOptions();
|
||||
|
||||
if (baseOptions) {
|
||||
options = baseOptions as typeof options;
|
||||
}
|
||||
|
||||
const buildNextOptions = useCallback(() => {
|
||||
let filteredOptions = [...options, ...(extraOptions ?? [])];
|
||||
if (queryString) {
|
||||
@ -267,8 +293,8 @@ export default function VariablePickerMenuPlugin({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor && value && isFirstRender.current) {
|
||||
isFirstRender.current = false;
|
||||
if (editor && value && value !== previousValue.current) {
|
||||
previousValue.current = value;
|
||||
editor.update(
|
||||
() => {
|
||||
parseTextToVariableNodes(value);
|
||||
@ -278,6 +304,21 @@ export default function VariablePickerMenuPlugin({
|
||||
}
|
||||
}, [parseTextToVariableNodes, editor, value]);
|
||||
|
||||
// Fixed the issue where the cursor would go to the end when changing its own data
|
||||
useEffect(() => {
|
||||
return editor.registerUpdateListener(({ editorState, tags }) => {
|
||||
// If we trigger the programmatic update ourselves, we should not write back to avoid an infinite loop.
|
||||
if (tags.has(ProgrammaticTag)) return;
|
||||
|
||||
editorState.read(() => {
|
||||
const text = $getRoot().getTextContent();
|
||||
if (text !== previousValue.current) {
|
||||
previousValue.current = text;
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<LexicalTypeaheadMenuPlugin<VariableOption | VariableInnerOption>
|
||||
onQueryChange={setQueryString}
|
||||
@ -288,7 +329,7 @@ export default function VariablePickerMenuPlugin({
|
||||
closeMenu,
|
||||
)
|
||||
}
|
||||
triggerFn={checkForTriggerMatch}
|
||||
triggerFn={testTriggerFn}
|
||||
options={buildNextOptions()}
|
||||
menuRenderFn={(anchorElementRef, { selectOptionAndCleanUp }) => {
|
||||
const nextOptions = buildNextOptions();
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { buildOutputOptions } from '@/utils/canvas-util';
|
||||
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) {
|
||||
|
||||
@ -1,19 +1,11 @@
|
||||
import { AgentGlobals } from '@/constants/agent';
|
||||
import { useFetchAgent } from '@/hooks/use-agent-request';
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||
import { Edge } from '@xyflow/react';
|
||||
import { buildNodeOutputOptions } from '@/utils/canvas-util';
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import { t } from 'i18next';
|
||||
import { isEmpty } from 'lodash';
|
||||
import get from 'lodash/get';
|
||||
import {
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
AgentDialogueMode,
|
||||
BeginId,
|
||||
@ -83,72 +75,18 @@ export const useGetBeginNodeDataQueryIsSafe = () => {
|
||||
return isBeginNodeDataQuerySafe;
|
||||
};
|
||||
|
||||
function filterAllUpstreamNodeIds(edges: Edge[], nodeIds: string[]) {
|
||||
return nodeIds.reduce<string[]>((pre, nodeId) => {
|
||||
const currentEdges = edges.filter((x) => x.target === nodeId);
|
||||
|
||||
const upstreamNodeIds: string[] = currentEdges.map((x) => x.source);
|
||||
|
||||
const ids = upstreamNodeIds.concat(
|
||||
filterAllUpstreamNodeIds(edges, upstreamNodeIds),
|
||||
);
|
||||
|
||||
ids.forEach((x) => {
|
||||
if (pre.every((y) => y !== x)) {
|
||||
pre.push(x);
|
||||
}
|
||||
});
|
||||
|
||||
return pre;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function buildOutputOptions(
|
||||
outputs: Record<string, any> = {},
|
||||
nodeId?: string,
|
||||
parentLabel?: string | ReactNode,
|
||||
icon?: ReactNode,
|
||||
) {
|
||||
return Object.keys(outputs).map((x) => ({
|
||||
label: x,
|
||||
value: `${nodeId}@${x}`,
|
||||
parentLabel,
|
||||
icon,
|
||||
type: outputs[x]?.type,
|
||||
}));
|
||||
}
|
||||
|
||||
export function useBuildNodeOutputOptions(nodeId?: string) {
|
||||
const nodes = useGraphStore((state) => state.nodes);
|
||||
const edges = useGraphStore((state) => state.edges);
|
||||
|
||||
const nodeOutputOptions = useMemo(() => {
|
||||
if (!nodeId) {
|
||||
return [];
|
||||
}
|
||||
const upstreamIds = filterAllUpstreamNodeIds(edges, [nodeId]);
|
||||
|
||||
const nodeWithOutputList = nodes.filter(
|
||||
(x) =>
|
||||
upstreamIds.some((y) => y === x.id) && !isEmpty(x.data?.form?.outputs),
|
||||
);
|
||||
|
||||
return nodeWithOutputList
|
||||
.filter((x) => x.id !== nodeId)
|
||||
.map((x) => ({
|
||||
label: x.data.name,
|
||||
value: x.id,
|
||||
title: x.data.name,
|
||||
options: buildOutputOptions(
|
||||
x.data.form.outputs,
|
||||
x.id,
|
||||
x.data.name,
|
||||
<OperatorIcon name={x.data.label as Operator} />,
|
||||
),
|
||||
}));
|
||||
return useMemo(() => {
|
||||
return buildNodeOutputOptions({
|
||||
nodes,
|
||||
edges,
|
||||
nodeId,
|
||||
Icon: ({ name }) => <OperatorIcon name={name as Operator}></OperatorIcon>,
|
||||
});
|
||||
}, [edges, nodeId, nodes]);
|
||||
|
||||
return nodeOutputOptions;
|
||||
}
|
||||
|
||||
// exclude nodes with branches
|
||||
|
||||
@ -23,7 +23,7 @@ import { ITraceData } from '@/interfaces/database/agent';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { t } from 'i18next';
|
||||
import { get, isEmpty, isEqual, uniqWith } from 'lodash';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import JsonView from 'react18-json-view';
|
||||
import { Operator } from '../constant';
|
||||
import { useCacheChatLog } from '../hooks/use-cache-chat-log';
|
||||
@ -116,12 +116,12 @@ export const WorkFlowTimeline = ({
|
||||
isShare,
|
||||
}: LogFlowTimelineProps) => {
|
||||
// const getNode = useGraphStore((state) => state.getNode);
|
||||
const [isStopFetchTrace, setISStopFetchTrace] = useState(false);
|
||||
|
||||
const { data: traceData, setMessageId } = useFetchMessageTrace(
|
||||
isStopFetchTrace,
|
||||
canvasId,
|
||||
);
|
||||
const {
|
||||
data: traceData,
|
||||
setMessageId,
|
||||
setISStopFetchTrace,
|
||||
} = useFetchMessageTrace(canvasId);
|
||||
|
||||
useEffect(() => {
|
||||
setMessageId(currentMessageId);
|
||||
@ -133,7 +133,7 @@ export const WorkFlowTimeline = ({
|
||||
|
||||
useEffect(() => {
|
||||
setISStopFetchTrace(!sendLoading);
|
||||
}, [sendLoading]);
|
||||
}, [sendLoading, setISStopFetchTrace]);
|
||||
|
||||
const startedNodeList = useMemo(() => {
|
||||
const finish = currentEventListWithoutMessage?.some(
|
||||
@ -151,7 +151,7 @@ export const WorkFlowTimeline = ({
|
||||
}
|
||||
return pre;
|
||||
}, []);
|
||||
}, [currentEventListWithoutMessage, sendLoading]);
|
||||
}, [currentEventListWithoutMessage, sendLoading, setISStopFetchTrace]);
|
||||
|
||||
const getElapsedTime = (nodeId: string) => {
|
||||
if (nodeId === 'begin') {
|
||||
|
||||
@ -51,7 +51,7 @@ const RunSheet = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<Sheet onOpenChange={hideModal} open>
|
||||
<Sheet onOpenChange={hideModal} open modal={false}>
|
||||
<SheetContent className={cn('top-20 p-2')}>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{t('flow.testRun')}</SheetTitle>
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { HomeCard } from '@/components/home-card';
|
||||
import { MoreButton } from '@/components/more-button';
|
||||
import { SharedBadge } from '@/components/shared-badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { AgentCategory } from '@/constants/agent';
|
||||
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
|
||||
import { IFlow } from '@/interfaces/database/agent';
|
||||
import { Route } from 'lucide-react';
|
||||
import { AgentDropdown } from './agent-dropdown';
|
||||
import { useRenameAgent } from './use-rename-agent';
|
||||
|
||||
@ -11,7 +14,7 @@ export type DatasetCardProps = {
|
||||
} & Pick<ReturnType<typeof useRenameAgent>, 'showAgentRenameModal'>;
|
||||
|
||||
export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) {
|
||||
const { navigateToAgent } = useNavigatePage();
|
||||
const { navigateToAgent, navigateToDataflow } = useNavigatePage();
|
||||
|
||||
return (
|
||||
<HomeCard
|
||||
@ -22,7 +25,18 @@ export function AgentCard({ data, showAgentRenameModal }: DatasetCardProps) {
|
||||
</AgentDropdown>
|
||||
}
|
||||
sharedBadge={<SharedBadge>{data.nickname}</SharedBadge>}
|
||||
onClick={navigateToAgent(data?.id)}
|
||||
onClick={
|
||||
data.canvas_category === AgentCategory.DataflowCanvas
|
||||
? navigateToDataflow(data.id)
|
||||
: navigateToAgent(data?.id)
|
||||
}
|
||||
icon={
|
||||
data.canvas_category === AgentCategory.DataflowCanvas && (
|
||||
<Button variant={'ghost'} size={'sm'}>
|
||||
<Route />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ export function CreateAgentDialog({
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={hideModal}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('flow.createGraph')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
@ -25,6 +25,7 @@ type FlowTypeCardProps = {
|
||||
onChange?: (value: FlowType) => void;
|
||||
};
|
||||
function FlowTypeCards({ value, onChange }: FlowTypeCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const handleChange = useCallback(
|
||||
(value: FlowType) => () => {
|
||||
onChange?.(value);
|
||||
@ -59,7 +60,11 @@ function FlowTypeCards({ value, onChange }: FlowTypeCardProps) {
|
||||
) : (
|
||||
<Route className="size-6" />
|
||||
)}
|
||||
<p>{val}</p>
|
||||
<p>
|
||||
{t(
|
||||
`flow.${val === FlowType.Agent ? 'createAgent' : 'createPipeline'}`,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{isActive && <Check />}
|
||||
</CardContent>
|
||||
@ -106,7 +111,11 @@ export function CreateAgentForm({
|
||||
id={TagRenameId}
|
||||
>
|
||||
{shouldChooseAgent && (
|
||||
<RAGFlowFormItem required name="type" label={t('common.type')}>
|
||||
<RAGFlowFormItem
|
||||
required
|
||||
name="type"
|
||||
label={t('flow.chooseAgentType')}
|
||||
>
|
||||
<FlowTypeCards></FlowTypeCards>
|
||||
</RAGFlowFormItem>
|
||||
)}
|
||||
|
||||
@ -1,10 +1,81 @@
|
||||
import { AgentCategory } from '@/constants/agent';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import { EmptyDsl, useSetAgent } from '@/hooks/use-agent-request';
|
||||
import { DSL } from '@/interfaces/database/agent';
|
||||
import {
|
||||
BeginId,
|
||||
Operator,
|
||||
initialParserValues,
|
||||
} from '@/pages/data-flow/constant';
|
||||
import { useCallback } from 'react';
|
||||
import { FlowType } from '../constant';
|
||||
import { FormSchemaType } from '../create-agent-form';
|
||||
|
||||
export const DataflowEmptyDsl = {
|
||||
graph: {
|
||||
nodes: [
|
||||
{
|
||||
id: BeginId,
|
||||
type: 'beginNode',
|
||||
position: {
|
||||
x: 50,
|
||||
y: 200,
|
||||
},
|
||||
data: {
|
||||
label: Operator.Begin,
|
||||
name: Operator.Begin,
|
||||
},
|
||||
sourcePosition: 'left',
|
||||
targetPosition: 'right',
|
||||
},
|
||||
{
|
||||
data: {
|
||||
form: initialParserValues,
|
||||
label: 'Parser',
|
||||
name: 'Parser_0',
|
||||
},
|
||||
dragging: false,
|
||||
id: 'Parser:HipSignsRhyme',
|
||||
measured: {
|
||||
height: 57,
|
||||
width: 200,
|
||||
},
|
||||
position: {
|
||||
x: 316.99524094206413,
|
||||
y: 195.39629819663406,
|
||||
},
|
||||
selected: true,
|
||||
sourcePosition: 'right',
|
||||
targetPosition: 'left',
|
||||
type: 'parserNode',
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: 'xy-edge__Filestart-Parser:HipSignsRhymeend',
|
||||
source: BeginId,
|
||||
sourceHandle: 'start',
|
||||
target: 'Parser:HipSignsRhyme',
|
||||
targetHandle: 'end',
|
||||
},
|
||||
],
|
||||
},
|
||||
components: {
|
||||
[Operator.Begin]: {
|
||||
obj: {
|
||||
component_name: Operator.Begin,
|
||||
params: {},
|
||||
},
|
||||
downstream: [], // other edge target is downstream, edge source is current node id
|
||||
upstream: [], // edge source is upstream, edge target is current node id
|
||||
},
|
||||
},
|
||||
retrieval: [], // reference
|
||||
history: [],
|
||||
path: [],
|
||||
globals: {},
|
||||
};
|
||||
|
||||
export function useCreateAgentOrPipeline() {
|
||||
const { loading, setAgent } = useSetAgent();
|
||||
const {
|
||||
@ -13,27 +84,26 @@ export function useCreateAgentOrPipeline() {
|
||||
showModal: showCreatingModal,
|
||||
} = useSetModalState();
|
||||
|
||||
const createAgent = useCallback(
|
||||
async (name: string) => {
|
||||
return setAgent({ title: name, dsl: EmptyDsl as DSL });
|
||||
},
|
||||
[setAgent],
|
||||
);
|
||||
|
||||
const handleCreateAgentOrPipeline = useCallback(
|
||||
async (data: FormSchemaType) => {
|
||||
if (data.type === FlowType.Agent) {
|
||||
const ret = await createAgent(data.name);
|
||||
if (ret.code === 0) {
|
||||
hideCreatingModal();
|
||||
}
|
||||
const isAgent = data.type === FlowType.Agent;
|
||||
const ret = await setAgent({
|
||||
title: data.name,
|
||||
dsl: isAgent ? (EmptyDsl as DSL) : (DataflowEmptyDsl as DSL),
|
||||
canvas_category: isAgent
|
||||
? AgentCategory.AgentCanvas
|
||||
: AgentCategory.DataflowCanvas,
|
||||
});
|
||||
|
||||
if (ret.code === 0) {
|
||||
hideCreatingModal();
|
||||
}
|
||||
},
|
||||
[createAgent, hideCreatingModal],
|
||||
[hideCreatingModal, setAgent],
|
||||
);
|
||||
|
||||
return {
|
||||
loading,
|
||||
loading: loading,
|
||||
creatingVisible,
|
||||
hideCreatingModal,
|
||||
showCreatingModal,
|
||||
|
||||
23
web/src/pages/agents/hooks/use-selelct-filters.ts
Normal file
23
web/src/pages/agents/hooks/use-selelct-filters.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { FilterCollection } from '@/components/list-filter-bar/interface';
|
||||
import { useFetchAgentList } from '@/hooks/use-agent-request';
|
||||
import { buildOwnersFilter, groupListByType } from '@/utils/list-filter-util';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function useSelectFilters() {
|
||||
const { data } = useFetchAgentList({});
|
||||
|
||||
const canvasCategory = useMemo(() => {
|
||||
return groupListByType(data.canvas, 'canvas_category', 'canvas_category');
|
||||
}, [data.canvas]);
|
||||
|
||||
const filters: FilterCollection[] = [
|
||||
buildOwnersFilter(data.canvas),
|
||||
{
|
||||
field: 'canvasCategory',
|
||||
list: canvasCategory,
|
||||
label: 'Canvas category',
|
||||
},
|
||||
];
|
||||
|
||||
return filters;
|
||||
}
|
||||
@ -17,13 +17,21 @@ import { useCallback } from 'react';
|
||||
import { AgentCard } from './agent-card';
|
||||
import { CreateAgentDialog } from './create-agent-dialog';
|
||||
import { useCreateAgentOrPipeline } from './hooks/use-create-agent';
|
||||
import { useSelectFilters } from './hooks/use-selelct-filters';
|
||||
import { UploadAgentDialog } from './upload-agent-dialog';
|
||||
import { useHandleImportJsonFile } from './use-import-json';
|
||||
import { useRenameAgent } from './use-rename-agent';
|
||||
|
||||
export default function Agents() {
|
||||
const { data, pagination, setPagination, searchString, handleInputChange } =
|
||||
useFetchAgentListByPage();
|
||||
const {
|
||||
data,
|
||||
pagination,
|
||||
setPagination,
|
||||
searchString,
|
||||
handleInputChange,
|
||||
filterValue,
|
||||
handleFilterSubmit,
|
||||
} = useFetchAgentListByPage();
|
||||
const { navigateToAgentTemplates } = useNavigatePage();
|
||||
|
||||
const {
|
||||
@ -50,6 +58,8 @@ export default function Agents() {
|
||||
hideFileUploadModal,
|
||||
} = useHandleImportJsonFile();
|
||||
|
||||
const filters = useSelectFilters();
|
||||
|
||||
const handlePageChange = useCallback(
|
||||
(page: number, pageSize?: number) => {
|
||||
setPagination({ page, pageSize });
|
||||
@ -65,6 +75,9 @@ export default function Agents() {
|
||||
searchString={searchString}
|
||||
onSearchChange={handleInputChange}
|
||||
icon="agent"
|
||||
filters={filters}
|
||||
onChange={handleFilterSubmit}
|
||||
value={filterValue}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
@ -79,21 +92,21 @@ export default function Agents() {
|
||||
onClick={showCreatingModal}
|
||||
>
|
||||
<Clipboard />
|
||||
Create from Blank
|
||||
{t('flow.createFromBlank')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
justifyBetween={false}
|
||||
onClick={navigateToAgentTemplates}
|
||||
>
|
||||
<ClipboardPlus />
|
||||
Create from Template
|
||||
{t('flow.createFromTemplate')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
justifyBetween={false}
|
||||
onClick={handleImportJson}
|
||||
>
|
||||
<FileInput />
|
||||
Import json file
|
||||
{t('flow.importJsonFile')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@ -16,12 +16,7 @@ export const NameFormSchema = {
|
||||
export function NameFormField() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<RAGFlowFormItem
|
||||
name="name"
|
||||
required
|
||||
label={t('common.name')}
|
||||
tooltip={t('flow.sqlStatementTip')}
|
||||
>
|
||||
<RAGFlowFormItem name="name" required label={t('common.name')}>
|
||||
<Input placeholder={t('common.namePlaceholder')} autoComplete="off" />
|
||||
</RAGFlowFormItem>
|
||||
);
|
||||
|
||||
@ -72,7 +72,7 @@ const Chunk = () => {
|
||||
chunkUpdatingVisible,
|
||||
documentId,
|
||||
} = useUpdateChunk();
|
||||
const { navigateToDataset, getQueryString, navigateToDatasetList } =
|
||||
const { navigateToDataFile, getQueryString, navigateToDatasetList } =
|
||||
useNavigatePage();
|
||||
const fileUrl = useGetDocumentUrl();
|
||||
useEffect(() => {
|
||||
@ -188,7 +188,7 @@ const Chunk = () => {
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink
|
||||
onClick={navigateToDataset(
|
||||
onClick={navigateToDataFile(
|
||||
getQueryString(QueryStringMap.id) as string,
|
||||
)}
|
||||
>
|
||||
|
||||
@ -11,7 +11,7 @@ import useGraphStore from '../../store';
|
||||
import { useFetchAgent } from '@/hooks/use-agent-request';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useMemo } from 'react';
|
||||
import { NodeHandleId, Operator } from '../../constant';
|
||||
import { Operator } from '../../constant';
|
||||
|
||||
function InnerButtonEdge({
|
||||
id,
|
||||
@ -27,7 +27,6 @@ function InnerButtonEdge({
|
||||
markerEnd,
|
||||
selected,
|
||||
data,
|
||||
sourceHandleId,
|
||||
}: EdgeProps<Edge<{ isHovered: boolean }>>) {
|
||||
const deleteEdgeById = useGraphStore((state) => state.deleteEdgeById);
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
@ -49,47 +48,32 @@ function InnerButtonEdge({
|
||||
// highlight the nodes that the workflow passes through
|
||||
const { data: flowDetail } = useFetchAgent();
|
||||
|
||||
const graphPath = useMemo(() => {
|
||||
// TODO: this will be called multiple times
|
||||
const showHighlight = useMemo(() => {
|
||||
const path = flowDetail?.dsl?.path ?? [];
|
||||
// The second to last
|
||||
const previousGraphPath: string[] = path.at(-2) ?? [];
|
||||
let graphPath: string[] = path.at(-1) ?? [];
|
||||
// The last of the second to last article
|
||||
const previousLatestElement = previousGraphPath.at(-1);
|
||||
if (previousGraphPath.length > 0 && previousLatestElement) {
|
||||
graphPath = [previousLatestElement, ...graphPath];
|
||||
}
|
||||
return Array.isArray(graphPath) ? graphPath : [];
|
||||
}, [flowDetail.dsl?.path]);
|
||||
|
||||
const highlightStyle = useMemo(() => {
|
||||
const idx = graphPath.findIndex((x) => x === source);
|
||||
const idx = path.findIndex((x) => x === target);
|
||||
if (idx !== -1) {
|
||||
// The set of elements following source
|
||||
const slicedGraphPath = graphPath.slice(idx + 1);
|
||||
if (slicedGraphPath.some((x) => x === target)) {
|
||||
return { strokeWidth: 1, stroke: 'red' };
|
||||
let index = idx - 1;
|
||||
while (index >= 0) {
|
||||
if (path[index] === source) {
|
||||
return { strokeWidth: 1, stroke: 'var(--accent-primary)' };
|
||||
}
|
||||
index--;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
}, [source, target, graphPath]);
|
||||
}, [flowDetail?.dsl?.path, source, target]);
|
||||
|
||||
const visible = useMemo(() => {
|
||||
return (
|
||||
data?.isHovered &&
|
||||
sourceHandleId !== NodeHandleId.Tool &&
|
||||
sourceHandleId !== NodeHandleId.AgentBottom && // The connection between the agent node and the tool node does not need to display the delete button
|
||||
!target.startsWith(Operator.Tool)
|
||||
);
|
||||
}, [data?.isHovered, sourceHandleId, target]);
|
||||
return data?.isHovered && source !== Operator.Begin;
|
||||
}, [data?.isHovered, source]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge
|
||||
path={edgePath}
|
||||
markerEnd={markerEnd}
|
||||
style={{ ...style, ...selectedStyle, ...highlightStyle }}
|
||||
style={{ ...style, ...selectedStyle, ...showHighlight }}
|
||||
className="text-text-secondary"
|
||||
/>
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
ControlButton,
|
||||
Controls,
|
||||
NodeTypes,
|
||||
OnConnectEnd,
|
||||
Position,
|
||||
ReactFlow,
|
||||
ReactFlowInstance,
|
||||
@ -30,58 +31,34 @@ import { useBeforeDelete } from '../hooks/use-before-delete';
|
||||
import { useMoveNote } from '../hooks/use-move-note';
|
||||
import { useDropdownManager } from './context';
|
||||
|
||||
import { useRunDataflow } from '../hooks/use-run-dataflow';
|
||||
import {
|
||||
useHideFormSheetOnNodeDeletion,
|
||||
useShowDrawer,
|
||||
} from '../hooks/use-show-drawer';
|
||||
import RunSheet from '../run-sheet';
|
||||
import useGraphStore from '../store';
|
||||
import { ButtonEdge } from './edge';
|
||||
import styles from './index.less';
|
||||
import { RagNode } from './node';
|
||||
import { AgentNode } from './node/agent-node';
|
||||
import { BeginNode } from './node/begin-node';
|
||||
import { CategorizeNode } from './node/categorize-node';
|
||||
import ChunkerNode from './node/chunker-node';
|
||||
import { InnerNextStepDropdown } from './node/dropdown/next-step-dropdown';
|
||||
import { GenerateNode } from './node/generate-node';
|
||||
import { InvokeNode } from './node/invoke-node';
|
||||
import { IterationNode, IterationStartNode } from './node/iteration-node';
|
||||
import { KeywordNode } from './node/keyword-node';
|
||||
import { LogicNode } from './node/logic-node';
|
||||
import { MessageNode } from './node/message-node';
|
||||
import { NextStepDropdown } from './node/dropdown/next-step-dropdown';
|
||||
import { ExtractorNode } from './node/extractor-node';
|
||||
import { HierarchicalMergerNode } from './node/hierarchical-merger-node';
|
||||
import NoteNode from './node/note-node';
|
||||
import ParserNode from './node/parser-node';
|
||||
import { RelevantNode } from './node/relevant-node';
|
||||
import { RetrievalNode } from './node/retrieval-node';
|
||||
import { RewriteNode } from './node/rewrite-node';
|
||||
import { SwitchNode } from './node/switch-node';
|
||||
import { TemplateNode } from './node/template-node';
|
||||
import { SplitterNode } from './node/splitter-node';
|
||||
import TokenizerNode from './node/tokenizer-node';
|
||||
import { ToolNode } from './node/tool-node';
|
||||
|
||||
export const nodeTypes: NodeTypes = {
|
||||
ragNode: RagNode,
|
||||
categorizeNode: CategorizeNode,
|
||||
beginNode: BeginNode,
|
||||
relevantNode: RelevantNode,
|
||||
logicNode: LogicNode,
|
||||
noteNode: NoteNode,
|
||||
switchNode: SwitchNode,
|
||||
generateNode: GenerateNode,
|
||||
retrievalNode: RetrievalNode,
|
||||
messageNode: MessageNode,
|
||||
rewriteNode: RewriteNode,
|
||||
keywordNode: KeywordNode,
|
||||
invokeNode: InvokeNode,
|
||||
templateNode: TemplateNode,
|
||||
// emailNode: EmailNode,
|
||||
group: IterationNode,
|
||||
iterationStartNode: IterationStartNode,
|
||||
agentNode: AgentNode,
|
||||
toolNode: ToolNode,
|
||||
parserNode: ParserNode,
|
||||
chunkerNode: ChunkerNode,
|
||||
tokenizerNode: TokenizerNode,
|
||||
splitterNode: SplitterNode,
|
||||
hierarchicalMergerNode: HierarchicalMergerNode,
|
||||
contextNode: ExtractorNode,
|
||||
};
|
||||
|
||||
const edgeTypes = {
|
||||
@ -91,9 +68,10 @@ const edgeTypes = {
|
||||
interface IProps {
|
||||
drawerVisible: boolean;
|
||||
hideDrawer(): void;
|
||||
showLogSheet(): void;
|
||||
}
|
||||
|
||||
function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
nodes,
|
||||
@ -121,7 +99,6 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
chatVisible,
|
||||
runVisible,
|
||||
hideRunOrChatDrawer,
|
||||
showChatModal,
|
||||
showFormDrawer,
|
||||
} = useShowDrawer({
|
||||
drawerVisible,
|
||||
@ -141,6 +118,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
useHideFormSheetOnNodeDeletion({ hideFormDrawer });
|
||||
|
||||
const { visible, hideModal, showModal } = useSetModalState();
|
||||
|
||||
const [dropdownPosition, setDropdownPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
const isConnectedRef = useRef(false);
|
||||
@ -153,6 +131,8 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
|
||||
const { setActiveDropdown, clearActiveDropdown } = useDropdownManager();
|
||||
|
||||
const { hasChildNode } = useGraphStore((state) => state);
|
||||
|
||||
const onPaneClick = useCallback(() => {
|
||||
hideFormDrawer();
|
||||
if (visible && !preventCloseRef.current) {
|
||||
@ -174,12 +154,17 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
clearActiveDropdown,
|
||||
]);
|
||||
|
||||
const { run, loading: running } = useRunDataflow(
|
||||
showLogSheet!,
|
||||
hideRunOrChatDrawer,
|
||||
);
|
||||
|
||||
const onConnect = (connection: Connection) => {
|
||||
originalOnConnect(connection);
|
||||
isConnectedRef.current = true;
|
||||
};
|
||||
|
||||
const OnConnectStart = (event: any, params: any) => {
|
||||
const onConnectStart = (event: any, params: any) => {
|
||||
isConnectedRef.current = false;
|
||||
|
||||
if (params && params.nodeId && params.handleId) {
|
||||
@ -192,7 +177,12 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const OnConnectEnd = (event: MouseEvent | TouchEvent) => {
|
||||
const onConnectEnd: OnConnectEnd = (event, connectionState) => {
|
||||
const nodeId = connectionState.fromNode?.id;
|
||||
// Events triggered by Handle are directly interrupted
|
||||
if (connectionState.toNode !== null || (nodeId && hasChildNode(nodeId))) {
|
||||
return;
|
||||
}
|
||||
if ('clientX' in event && 'clientY' in event) {
|
||||
const { clientX, clientY } = event;
|
||||
setDropdownPosition({ x: clientX, y: clientY });
|
||||
@ -240,8 +230,8 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onConnectStart={OnConnectStart}
|
||||
onConnectEnd={OnConnectEnd}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnectEnd={onConnectEnd}
|
||||
onNodeClick={onNodeClick}
|
||||
onPaneClick={onPaneClick}
|
||||
onInit={setReactFlowInstance}
|
||||
@ -288,7 +278,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
isFromConnectionDrag: true,
|
||||
}}
|
||||
>
|
||||
<InnerNextStepDropdown
|
||||
<NextStepDropdown
|
||||
hideModal={() => {
|
||||
hideModal();
|
||||
clearActiveDropdown();
|
||||
@ -296,7 +286,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
position={dropdownPosition}
|
||||
>
|
||||
<span></span>
|
||||
</InnerNextStepDropdown>
|
||||
</NextStepDropdown>
|
||||
</HandleContext.Provider>
|
||||
)}
|
||||
</AgentInstanceContext.Provider>
|
||||
@ -322,7 +312,8 @@ function DataFlowCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
{runVisible && (
|
||||
<RunSheet
|
||||
hideModal={hideRunOrChatDrawer}
|
||||
showModal={showChatModal}
|
||||
run={run}
|
||||
loading={running}
|
||||
></RunSheet>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,116 +0,0 @@
|
||||
import LLMLabel from '@/components/llm-select/llm-label';
|
||||
import { IAgentNode } from '@/interfaces/database/flow';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import { get } from 'lodash';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AgentExceptionMethod, NodeHandleId } from '../../constant';
|
||||
import useGraphStore from '../../store';
|
||||
import { isBottomSubAgent } from '../../utils';
|
||||
import { CommonHandle } from './handle';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
import { NodeWrapper } from './node-wrapper';
|
||||
import { ToolBar } from './toolbar';
|
||||
|
||||
function InnerAgentNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IAgentNode>) {
|
||||
const edges = useGraphStore((state) => state.edges);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const isHeadAgent = useMemo(() => {
|
||||
return !isBottomSubAgent(edges, id);
|
||||
}, [edges, id]);
|
||||
|
||||
const exceptionMethod = useMemo(() => {
|
||||
return get(data, 'form.exception_method');
|
||||
}, [data]);
|
||||
|
||||
const isGotoMethod = useMemo(() => {
|
||||
return exceptionMethod === AgentExceptionMethod.Goto;
|
||||
}, [exceptionMethod]);
|
||||
|
||||
return (
|
||||
<ToolBar selected={selected} id={id} label={data.label}>
|
||||
<NodeWrapper selected={selected}>
|
||||
{isHeadAgent && (
|
||||
<>
|
||||
<CommonHandle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
isConnectable={isConnectable}
|
||||
style={LeftHandleStyle}
|
||||
nodeId={id}
|
||||
id={NodeHandleId.End}
|
||||
></CommonHandle>
|
||||
<CommonHandle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
style={RightHandleStyle}
|
||||
nodeId={id}
|
||||
id={NodeHandleId.Start}
|
||||
isConnectableEnd={false}
|
||||
></CommonHandle>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
isConnectable={false}
|
||||
id={NodeHandleId.AgentTop}
|
||||
></Handle>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
isConnectable={false}
|
||||
id={NodeHandleId.AgentBottom}
|
||||
style={{ left: 180 }}
|
||||
></Handle>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
isConnectable={false}
|
||||
id={NodeHandleId.Tool}
|
||||
style={{ left: 20 }}
|
||||
></Handle>
|
||||
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||
<section className="flex flex-col gap-2">
|
||||
<div className={'bg-bg-card rounded-sm p-1'}>
|
||||
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
|
||||
</div>
|
||||
{(isGotoMethod ||
|
||||
exceptionMethod === AgentExceptionMethod.Comment) && (
|
||||
<div className="bg-bg-card rounded-sm p-1 flex justify-between gap-2">
|
||||
<span className="text-text-secondary">{t('flow.onFailure')}</span>
|
||||
<span className="truncate flex-1 text-right">
|
||||
{t(`flow.${exceptionMethod}`)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
{isGotoMethod && (
|
||||
<CommonHandle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
className="!bg-state-error"
|
||||
style={{ ...RightHandleStyle, top: 94 }}
|
||||
nodeId={id}
|
||||
id={NodeHandleId.AgentException}
|
||||
isConnectableEnd={false}
|
||||
></CommonHandle>
|
||||
)}
|
||||
</NodeWrapper>
|
||||
</ToolBar>
|
||||
);
|
||||
}
|
||||
|
||||
export const AgentNode = memo(InnerAgentNode);
|
||||
@ -36,7 +36,7 @@ function InnerBeginNode({ data, id, selected }: NodeProps<IBeginNode>) {
|
||||
<section className="flex items-center gap-2">
|
||||
<OperatorIcon name={data.label as Operator}></OperatorIcon>
|
||||
<div className="truncate text-center font-semibold text-sm">
|
||||
{t(`flow.begin`)}
|
||||
{t(`dataflow.begin`)}
|
||||
</div>
|
||||
</section>
|
||||
<section className={cn(styles.generateParameters, 'flex gap-2 flex-col')}>
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
import LLMLabel from '@/components/llm-select/llm-label';
|
||||
import { ICategorizeNode } from '@/interfaces/database/flow';
|
||||
import { NodeProps, Position } from '@xyflow/react';
|
||||
import { get } from 'lodash';
|
||||
import { memo } from 'react';
|
||||
import { NodeHandleId } from '../../constant';
|
||||
import { CommonHandle } from './handle';
|
||||
import { RightHandleStyle } from './handle-icon';
|
||||
import NodeHeader from './node-header';
|
||||
import { NodeWrapper } from './node-wrapper';
|
||||
import { ToolBar } from './toolbar';
|
||||
import { useBuildCategorizeHandlePositions } from './use-build-categorize-handle-positions';
|
||||
|
||||
export function InnerCategorizeNode({
|
||||
id,
|
||||
data,
|
||||
selected,
|
||||
}: NodeProps<ICategorizeNode>) {
|
||||
const { positions } = useBuildCategorizeHandlePositions({ data, id });
|
||||
return (
|
||||
<ToolBar selected={selected} id={id} label={data.label}>
|
||||
<NodeWrapper selected={selected}>
|
||||
<CommonHandle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
isConnectable
|
||||
id={NodeHandleId.End}
|
||||
nodeId={id}
|
||||
></CommonHandle>
|
||||
|
||||
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||
|
||||
<section className="flex flex-col gap-2">
|
||||
<div className={'bg-bg-card rounded-sm px-1'}>
|
||||
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
|
||||
</div>
|
||||
{positions.map((position) => {
|
||||
return (
|
||||
<div key={position.uuid}>
|
||||
<div className={'bg-bg-card rounded-sm p-1 truncate'}>
|
||||
{position.name}
|
||||
</div>
|
||||
<CommonHandle
|
||||
// key={position.text}
|
||||
id={position.uuid}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable
|
||||
style={{ ...RightHandleStyle, top: position.top }}
|
||||
nodeId={id}
|
||||
isConnectableEnd={false}
|
||||
></CommonHandle>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</NodeWrapper>
|
||||
</ToolBar>
|
||||
);
|
||||
}
|
||||
|
||||
export const CategorizeNode = memo(InnerCategorizeNode);
|
||||
@ -1,49 +0,0 @@
|
||||
import { IRagNode } from '@/interfaces/database/flow';
|
||||
import { NodeProps, Position } from '@xyflow/react';
|
||||
import { memo } from 'react';
|
||||
import { NodeHandleId } from '../../constant';
|
||||
import { needsSingleStepDebugging } from '../../utils';
|
||||
import { CommonHandle } from './handle';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import NodeHeader from './node-header';
|
||||
import { NodeWrapper } from './node-wrapper';
|
||||
import { ToolBar } from './toolbar';
|
||||
|
||||
function ChunkerNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IRagNode>) {
|
||||
return (
|
||||
<ToolBar
|
||||
selected={selected}
|
||||
id={id}
|
||||
label={data.label}
|
||||
showRun={needsSingleStepDebugging(data.label)}
|
||||
>
|
||||
<NodeWrapper selected={selected}>
|
||||
<CommonHandle
|
||||
id={NodeHandleId.End}
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
isConnectable={isConnectable}
|
||||
style={LeftHandleStyle}
|
||||
nodeId={id}
|
||||
></CommonHandle>
|
||||
<CommonHandle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
id={NodeHandleId.Start}
|
||||
style={RightHandleStyle}
|
||||
nodeId={id}
|
||||
isConnectableEnd={false}
|
||||
></CommonHandle>
|
||||
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||
</NodeWrapper>
|
||||
</ToolBar>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ChunkerNode);
|
||||
@ -1,9 +1,3 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@/components/ui/accordion';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@ -18,6 +12,7 @@ import {
|
||||
} from '@/components/ui/tooltip';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { useGetNodeDescription, useGetNodeName } from '@/pages/data-flow/hooks';
|
||||
import useGraphStore from '@/pages/data-flow/store';
|
||||
import { Position } from '@xyflow/react';
|
||||
import { t } from 'i18next';
|
||||
import {
|
||||
@ -26,9 +21,10 @@ import {
|
||||
memo,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { Operator } from '../../../constant';
|
||||
import { Operator, SingleOperators } from '../../../constant';
|
||||
import { AgentInstanceContext, HandleContext } from '../../../context';
|
||||
import OperatorIcon from '../../../operator-icon';
|
||||
|
||||
@ -116,6 +112,20 @@ function OperatorItemList({
|
||||
return <ul className="space-y-2">{operators.map(renderOperatorItem)}</ul>;
|
||||
}
|
||||
|
||||
// Limit the number of operators of a certain type on the canvas to only one
|
||||
function useRestrictSingleOperatorOnCanvas() {
|
||||
const list: Operator[] = [];
|
||||
const { findNodeByName } = useGraphStore((state) => state);
|
||||
|
||||
SingleOperators.forEach((operator) => {
|
||||
if (!findNodeByName(operator)) {
|
||||
list.push(operator);
|
||||
}
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function AccordionOperators({
|
||||
isCustomDropdown = false,
|
||||
mousePosition,
|
||||
@ -123,83 +133,19 @@ function AccordionOperators({
|
||||
isCustomDropdown?: boolean;
|
||||
mousePosition?: { x: number; y: number };
|
||||
}) {
|
||||
const singleOperators = useRestrictSingleOperatorOnCanvas();
|
||||
const operators = useMemo(() => {
|
||||
const list = [...singleOperators];
|
||||
list.push(Operator.Extractor);
|
||||
return list;
|
||||
}, [singleOperators]);
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
type="multiple"
|
||||
className="px-2 text-text-title max-h-[45vh] overflow-auto"
|
||||
defaultValue={['item-1', 'item-2', 'item-3', 'item-4', 'item-5']}
|
||||
>
|
||||
<AccordionItem value="item-1">
|
||||
<AccordionTrigger className="text-xl">
|
||||
{t('flow.foundation')}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<OperatorItemList
|
||||
operators={[
|
||||
Operator.Agent,
|
||||
Operator.Retrieval,
|
||||
Operator.Parser,
|
||||
Operator.Chunker,
|
||||
Operator.Tokenizer,
|
||||
]}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
mousePosition={mousePosition}
|
||||
></OperatorItemList>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-2">
|
||||
<AccordionTrigger className="text-xl">
|
||||
{t('flow.dialog')}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<OperatorItemList
|
||||
operators={[Operator.Message, Operator.UserFillUp]}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
mousePosition={mousePosition}
|
||||
></OperatorItemList>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-3">
|
||||
<AccordionTrigger className="text-xl">
|
||||
{t('flow.flow')}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<OperatorItemList
|
||||
operators={[
|
||||
Operator.Switch,
|
||||
Operator.Iteration,
|
||||
Operator.Categorize,
|
||||
]}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
mousePosition={mousePosition}
|
||||
></OperatorItemList>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-4">
|
||||
<AccordionTrigger className="text-xl">
|
||||
{t('flow.dataManipulation')}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<OperatorItemList
|
||||
operators={[Operator.Code, Operator.StringTransform]}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
mousePosition={mousePosition}
|
||||
></OperatorItemList>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="item-5">
|
||||
<AccordionTrigger className="text-xl">
|
||||
{t('flow.tools')}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="flex flex-col gap-4 text-balance">
|
||||
<OperatorItemList
|
||||
operators={[Operator.ExeSQL, Operator.Email, Operator.Invoke]}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
mousePosition={mousePosition}
|
||||
></OperatorItemList>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<OperatorItemList
|
||||
operators={operators}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
mousePosition={mousePosition}
|
||||
></OperatorItemList>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,80 +0,0 @@
|
||||
import { IEmailNode } from '@/interfaces/database/flow';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import { Flex } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { memo, useState } from 'react';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
export function InnerEmailNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IEmailNode>) {
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={classNames(styles.ragNode, {
|
||||
[styles.selectedNode]: selected,
|
||||
})}
|
||||
>
|
||||
<Handle
|
||||
id="c"
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
style={LeftHandleStyle}
|
||||
></Handle>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
style={RightHandleStyle}
|
||||
id="b"
|
||||
></Handle>
|
||||
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||
|
||||
<Flex vertical gap={8} className={styles.emailNodeContainer}>
|
||||
<div
|
||||
className={styles.emailConfig}
|
||||
onClick={() => setShowDetails(!showDetails)}
|
||||
>
|
||||
<div className={styles.configItem}>
|
||||
<span className={styles.configLabel}>SMTP:</span>
|
||||
<span className={styles.configValue}>{data.form?.smtp_server}</span>
|
||||
</div>
|
||||
<div className={styles.configItem}>
|
||||
<span className={styles.configLabel}>Port:</span>
|
||||
<span className={styles.configValue}>{data.form?.smtp_port}</span>
|
||||
</div>
|
||||
<div className={styles.configItem}>
|
||||
<span className={styles.configLabel}>From:</span>
|
||||
<span className={styles.configValue}>{data.form?.email}</span>
|
||||
</div>
|
||||
<div className={styles.expandIcon}>{showDetails ? '▼' : '▶'}</div>
|
||||
</div>
|
||||
|
||||
{showDetails && (
|
||||
<div className={styles.jsonExample}>
|
||||
<div className={styles.jsonTitle}>Expected Input JSON:</div>
|
||||
<pre className={styles.jsonContent}>
|
||||
{`{
|
||||
"to_email": "...",
|
||||
"cc_email": "...",
|
||||
"subject": "...",
|
||||
"content": "..."
|
||||
}`}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export const EmailNode = memo(InnerEmailNode);
|
||||
1
web/src/pages/data-flow/canvas/node/extractor-node.tsx
Normal file
1
web/src/pages/data-flow/canvas/node/extractor-node.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { RagNode as ExtractorNode } from './index';
|
||||
@ -1,60 +0,0 @@
|
||||
import LLMLabel from '@/components/llm-select/llm-label';
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { IGenerateNode } from '@/interfaces/database/flow';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { memo } from 'react';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
export function InnerGenerateNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IGenerateNode>) {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.logicNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Handle
|
||||
id="c"
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
style={LeftHandleStyle}
|
||||
></Handle>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
style={RightHandleStyle}
|
||||
id="b"
|
||||
></Handle>
|
||||
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
className={styles.nodeHeader}
|
||||
></NodeHeader>
|
||||
|
||||
<div className={styles.nodeText}>
|
||||
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export const GenerateNode = memo(InnerGenerateNode);
|
||||
@ -4,8 +4,9 @@ import { Handle, HandleProps } from '@xyflow/react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { HandleContext } from '../../context';
|
||||
import useGraphStore from '../../store';
|
||||
import { useDropdownManager } from '../context';
|
||||
import { InnerNextStepDropdown } from './dropdown/next-step-dropdown';
|
||||
import { NextStepDropdown } from './dropdown/next-step-dropdown';
|
||||
|
||||
export function CommonHandle({
|
||||
className,
|
||||
@ -17,6 +18,8 @@ export function CommonHandle({
|
||||
const { canShowDropdown, setActiveDropdown, clearActiveDropdown } =
|
||||
useDropdownManager();
|
||||
|
||||
const { hasChildNode } = useGraphStore((state) => state);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
nodeId,
|
||||
@ -39,6 +42,10 @@ export function CommonHandle({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (hasChildNode(nodeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canShowDropdown()) {
|
||||
return;
|
||||
}
|
||||
@ -49,14 +56,14 @@ export function CommonHandle({
|
||||
>
|
||||
<Plus className="size-3 pointer-events-none text-text-title-invert" />
|
||||
{visible && (
|
||||
<InnerNextStepDropdown
|
||||
<NextStepDropdown
|
||||
hideModal={() => {
|
||||
hideModal();
|
||||
clearActiveDropdown();
|
||||
}}
|
||||
>
|
||||
<span></span>
|
||||
</InnerNextStepDropdown>
|
||||
</NextStepDropdown>
|
||||
)}
|
||||
</Handle>
|
||||
</HandleContext.Provider>
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export { RagNode as HierarchicalMergerNode } from './index';
|
||||
@ -1,8 +1,8 @@
|
||||
import { IRagNode } from '@/interfaces/database/flow';
|
||||
import { NodeProps, Position } from '@xyflow/react';
|
||||
import { memo } from 'react';
|
||||
import { NodeHandleId } from '../../constant';
|
||||
import { needsSingleStepDebugging } from '../../utils';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { NodeHandleId, SingleOperators } from '../../constant';
|
||||
import useGraphStore from '../../store';
|
||||
import { CommonHandle } from './handle';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import NodeHeader from './node-header';
|
||||
@ -15,13 +15,17 @@ function InnerRagNode({
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IRagNode>) {
|
||||
const getOperatorTypeFromId = useGraphStore(
|
||||
(state) => state.getOperatorTypeFromId,
|
||||
);
|
||||
|
||||
const showCopy = useMemo(() => {
|
||||
const operatorName = getOperatorTypeFromId(id);
|
||||
return SingleOperators.every((x) => x !== operatorName);
|
||||
}, [getOperatorTypeFromId, id]);
|
||||
|
||||
return (
|
||||
<ToolBar
|
||||
selected={selected}
|
||||
id={id}
|
||||
label={data.label}
|
||||
showRun={needsSingleStepDebugging(data.label)}
|
||||
>
|
||||
<ToolBar selected={selected} id={id} label={data.label} showCopy={showCopy}>
|
||||
<NodeWrapper selected={selected}>
|
||||
<CommonHandle
|
||||
id={NodeHandleId.End}
|
||||
|
||||
@ -1,62 +0,0 @@
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { IInvokeNode } from '@/interfaces/database/flow';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import { Flex } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
function InnerInvokeNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IInvokeNode>) {
|
||||
const { t } = useTranslation();
|
||||
const { theme } = useTheme();
|
||||
const url = get(data, 'form.url');
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.ragNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Handle
|
||||
id="c"
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
style={LeftHandleStyle}
|
||||
></Handle>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
id="b"
|
||||
style={RightHandleStyle}
|
||||
></Handle>
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
className={styles.nodeHeader}
|
||||
></NodeHeader>
|
||||
<Flex vertical>
|
||||
<div>{t('flow.url')}</div>
|
||||
<div className={styles.nodeText}>{url}</div>
|
||||
</Flex>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export const InvokeNode = memo(InnerInvokeNode);
|
||||
@ -1,93 +0,0 @@
|
||||
import {
|
||||
IIterationNode,
|
||||
IIterationStartNode,
|
||||
} from '@/interfaces/database/flow';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { NodeProps, NodeResizeControl, Position } from '@xyflow/react';
|
||||
import { memo } from 'react';
|
||||
import { NodeHandleId, Operator } from '../../constant';
|
||||
import OperatorIcon from '../../operator-icon';
|
||||
import { CommonHandle } from './handle';
|
||||
import { RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
import { NodeWrapper } from './node-wrapper';
|
||||
import { ResizeIcon, controlStyle } from './resize-icon';
|
||||
import { ToolBar } from './toolbar';
|
||||
|
||||
export function InnerIterationNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IIterationNode>) {
|
||||
return (
|
||||
<ToolBar selected={selected} id={id} label={data.label} showRun={false}>
|
||||
<section
|
||||
className={cn('h-full bg-transparent rounded-b-md ', {
|
||||
[styles.selectedHeader]: selected,
|
||||
})}
|
||||
>
|
||||
<NodeResizeControl style={controlStyle} minWidth={100} minHeight={50}>
|
||||
<ResizeIcon />
|
||||
</NodeResizeControl>
|
||||
<CommonHandle
|
||||
id={NodeHandleId.End}
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
nodeId={id}
|
||||
></CommonHandle>
|
||||
<CommonHandle
|
||||
id={NodeHandleId.Start}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
nodeId={id}
|
||||
></CommonHandle>
|
||||
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
wrapperClassName={cn(
|
||||
'bg-background-header-bar p-2 rounded-t-[10px] absolute w-full top-[-44px] left-[-0.3px]',
|
||||
{
|
||||
[styles.selectedHeader]: selected,
|
||||
},
|
||||
)}
|
||||
></NodeHeader>
|
||||
</section>
|
||||
</ToolBar>
|
||||
);
|
||||
}
|
||||
|
||||
function InnerIterationStartNode({
|
||||
isConnectable = true,
|
||||
id,
|
||||
selected,
|
||||
}: NodeProps<IIterationStartNode>) {
|
||||
return (
|
||||
<NodeWrapper className="w-20" selected={selected}>
|
||||
<CommonHandle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
style={RightHandleStyle}
|
||||
isConnectableEnd={false}
|
||||
id={NodeHandleId.Start}
|
||||
nodeId={id}
|
||||
></CommonHandle>
|
||||
<div>
|
||||
<OperatorIcon name={Operator.Begin}></OperatorIcon>
|
||||
</div>
|
||||
</NodeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export const IterationStartNode = memo(InnerIterationStartNode);
|
||||
|
||||
export const IterationNode = memo(InnerIterationNode);
|
||||
@ -1,60 +0,0 @@
|
||||
import LLMLabel from '@/components/llm-select/llm-label';
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { IKeywordNode } from '@/interfaces/database/flow';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { memo } from 'react';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
export function InnerKeywordNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IKeywordNode>) {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.logicNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Handle
|
||||
id="c"
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
style={LeftHandleStyle}
|
||||
></Handle>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
style={RightHandleStyle}
|
||||
id="b"
|
||||
></Handle>
|
||||
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
className={styles.nodeHeader}
|
||||
></NodeHeader>
|
||||
|
||||
<div className={styles.nodeText}>
|
||||
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export const KeywordNode = memo(InnerKeywordNode);
|
||||
@ -1,41 +0,0 @@
|
||||
import { ILogicNode } from '@/interfaces/database/flow';
|
||||
import { NodeProps, Position } from '@xyflow/react';
|
||||
import { memo } from 'react';
|
||||
import { CommonHandle } from './handle';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import NodeHeader from './node-header';
|
||||
import { NodeWrapper } from './node-wrapper';
|
||||
import { ToolBar } from './toolbar';
|
||||
|
||||
export function InnerLogicNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<ILogicNode>) {
|
||||
return (
|
||||
<ToolBar selected={selected} id={id} label={data.label}>
|
||||
<NodeWrapper selected={selected}>
|
||||
<CommonHandle
|
||||
id="c"
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
isConnectable={isConnectable}
|
||||
style={LeftHandleStyle}
|
||||
nodeId={id}
|
||||
></CommonHandle>
|
||||
<CommonHandle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
style={RightHandleStyle}
|
||||
id="b"
|
||||
nodeId={id}
|
||||
></CommonHandle>
|
||||
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||
</NodeWrapper>
|
||||
</ToolBar>
|
||||
);
|
||||
}
|
||||
|
||||
export const LogicNode = memo(InnerLogicNode);
|
||||
@ -1,65 +0,0 @@
|
||||
import { IMessageNode } from '@/interfaces/database/flow';
|
||||
import { NodeProps, Position } from '@xyflow/react';
|
||||
import { Flex } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { memo } from 'react';
|
||||
import { NodeHandleId } from '../../constant';
|
||||
import { CommonHandle } from './handle';
|
||||
import { LeftHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
import { NodeWrapper } from './node-wrapper';
|
||||
import { ToolBar } from './toolbar';
|
||||
|
||||
function InnerMessageNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IMessageNode>) {
|
||||
const messages: string[] = get(data, 'form.messages', []);
|
||||
return (
|
||||
<ToolBar selected={selected} id={id} label={data.label}>
|
||||
<NodeWrapper selected={selected}>
|
||||
<CommonHandle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
isConnectable={isConnectable}
|
||||
style={LeftHandleStyle}
|
||||
nodeId={id}
|
||||
id={NodeHandleId.End}
|
||||
></CommonHandle>
|
||||
{/* <CommonHandle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
style={RightHandleStyle}
|
||||
id={NodeHandleId.Start}
|
||||
nodeId={id}
|
||||
isConnectableEnd={false}
|
||||
></CommonHandle> */}
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
className={classNames({
|
||||
[styles.nodeHeader]: messages.length > 0,
|
||||
})}
|
||||
></NodeHeader>
|
||||
|
||||
<Flex vertical gap={8} className={styles.messageNodeContainer}>
|
||||
{messages.map((message, idx) => {
|
||||
return (
|
||||
<div className={styles.nodeText} key={idx}>
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Flex>
|
||||
</NodeWrapper>
|
||||
</ToolBar>
|
||||
);
|
||||
}
|
||||
|
||||
export const MessageNode = memo(InnerMessageNode);
|
||||
@ -2,12 +2,10 @@ import { IRagNode } from '@/interfaces/database/flow';
|
||||
import { NodeProps, Position } from '@xyflow/react';
|
||||
import { memo } from 'react';
|
||||
import { NodeHandleId } from '../../constant';
|
||||
import { needsSingleStepDebugging } from '../../utils';
|
||||
import { CommonHandle } from './handle';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import NodeHeader from './node-header';
|
||||
import { NodeWrapper } from './node-wrapper';
|
||||
import { ToolBar } from './toolbar';
|
||||
|
||||
function ParserNode({
|
||||
id,
|
||||
@ -16,33 +14,26 @@ function ParserNode({
|
||||
selected,
|
||||
}: NodeProps<IRagNode>) {
|
||||
return (
|
||||
<ToolBar
|
||||
selected={selected}
|
||||
id={id}
|
||||
label={data.label}
|
||||
showRun={needsSingleStepDebugging(data.label)}
|
||||
>
|
||||
<NodeWrapper selected={selected}>
|
||||
<CommonHandle
|
||||
id={NodeHandleId.End}
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
isConnectable={isConnectable}
|
||||
style={LeftHandleStyle}
|
||||
nodeId={id}
|
||||
></CommonHandle>
|
||||
<CommonHandle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
id={NodeHandleId.Start}
|
||||
style={RightHandleStyle}
|
||||
nodeId={id}
|
||||
isConnectableEnd={false}
|
||||
></CommonHandle>
|
||||
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||
</NodeWrapper>
|
||||
</ToolBar>
|
||||
<NodeWrapper selected={selected}>
|
||||
<CommonHandle
|
||||
id={NodeHandleId.End}
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
isConnectable={isConnectable}
|
||||
style={LeftHandleStyle}
|
||||
nodeId={id}
|
||||
></CommonHandle>
|
||||
<CommonHandle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
id={NodeHandleId.Start}
|
||||
style={RightHandleStyle}
|
||||
nodeId={id}
|
||||
isConnectableEnd={false}
|
||||
></CommonHandle>
|
||||
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||
</NodeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,73 +0,0 @@
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import { Flex } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { RightHandleStyle } from './handle-icon';
|
||||
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { IRelevantNode } from '@/interfaces/database/flow';
|
||||
import { get } from 'lodash';
|
||||
import { memo } from 'react';
|
||||
import { useReplaceIdWithName } from '../../hooks';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
function InnerRelevantNode({ id, data, selected }: NodeProps<IRelevantNode>) {
|
||||
const yes = get(data, 'form.yes');
|
||||
const no = get(data, 'form.no');
|
||||
const replaceIdWithName = useReplaceIdWithName();
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.logicNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
isConnectable
|
||||
className={styles.handle}
|
||||
id={'a'}
|
||||
></Handle>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable
|
||||
className={styles.handle}
|
||||
id={'yes'}
|
||||
style={{ ...RightHandleStyle, top: 57 + 20 }}
|
||||
></Handle>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable
|
||||
className={styles.handle}
|
||||
id={'no'}
|
||||
style={{ ...RightHandleStyle, top: 115 + 20 }}
|
||||
></Handle>
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
className={styles.nodeHeader}
|
||||
></NodeHeader>
|
||||
|
||||
<Flex vertical gap={10}>
|
||||
<Flex vertical>
|
||||
<div className={styles.relevantLabel}>Yes</div>
|
||||
<div className={styles.nodeText}>{replaceIdWithName(yes)}</div>
|
||||
</Flex>
|
||||
<Flex vertical>
|
||||
<div className={styles.relevantLabel}>No</div>
|
||||
<div className={styles.nodeText}>{replaceIdWithName(no)}</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export const RelevantNode = memo(InnerRelevantNode);
|
||||
@ -1,84 +0,0 @@
|
||||
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||
import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks';
|
||||
import { IRetrievalNode } from '@/interfaces/database/flow';
|
||||
import { NodeProps, Position } from '@xyflow/react';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { memo } from 'react';
|
||||
import { NodeHandleId } from '../../constant';
|
||||
import { useGetVariableLabelByValue } from '../../hooks/use-get-begin-query';
|
||||
import { CommonHandle } from './handle';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
import { NodeWrapper } from './node-wrapper';
|
||||
import { ToolBar } from './toolbar';
|
||||
|
||||
function InnerRetrievalNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IRetrievalNode>) {
|
||||
const knowledgeBaseIds: string[] = get(data, 'form.kb_ids', []);
|
||||
const { list: knowledgeList } = useFetchKnowledgeList(true);
|
||||
|
||||
const getLabel = useGetVariableLabelByValue(id);
|
||||
|
||||
return (
|
||||
<ToolBar selected={selected} id={id} label={data.label}>
|
||||
<NodeWrapper selected={selected}>
|
||||
<CommonHandle
|
||||
id={NodeHandleId.End}
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
style={LeftHandleStyle}
|
||||
nodeId={id}
|
||||
></CommonHandle>
|
||||
<CommonHandle
|
||||
id={NodeHandleId.Start}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
style={RightHandleStyle}
|
||||
nodeId={id}
|
||||
isConnectableEnd={false}
|
||||
></CommonHandle>
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
className={classNames({
|
||||
[styles.nodeHeader]: knowledgeBaseIds.length > 0,
|
||||
})}
|
||||
></NodeHeader>
|
||||
<section className="flex flex-col gap-2">
|
||||
{knowledgeBaseIds.map((id) => {
|
||||
const item = knowledgeList.find((y) => id === y.id);
|
||||
const label = getLabel(id);
|
||||
|
||||
return (
|
||||
<div className={styles.nodeText} key={id}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<RAGFlowAvatar
|
||||
className="size-6 rounded-lg"
|
||||
avatar={id}
|
||||
name={item?.name || (label as string) || 'CN'}
|
||||
isPerson={true}
|
||||
/>
|
||||
|
||||
<div className={'truncate flex-1'}>{label || item?.name}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</NodeWrapper>
|
||||
</ToolBar>
|
||||
);
|
||||
}
|
||||
|
||||
export const RetrievalNode = memo(InnerRetrievalNode);
|
||||
@ -1,60 +0,0 @@
|
||||
import LLMLabel from '@/components/llm-select/llm-label';
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { IRewriteNode } from '@/interfaces/database/flow';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { memo } from 'react';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import styles from './index.less';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
function InnerRewriteNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IRewriteNode>) {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.logicNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Handle
|
||||
id="c"
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
style={LeftHandleStyle}
|
||||
></Handle>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
style={RightHandleStyle}
|
||||
id="b"
|
||||
></Handle>
|
||||
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
className={styles.nodeHeader}
|
||||
></NodeHeader>
|
||||
|
||||
<div className={styles.nodeText}>
|
||||
<LLMLabel value={get(data, 'form.llm_id')}></LLMLabel>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export const RewriteNode = memo(InnerRewriteNode);
|
||||
1
web/src/pages/data-flow/canvas/node/splitter-node.tsx
Normal file
1
web/src/pages/data-flow/canvas/node/splitter-node.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { RagNode as SplitterNode } from './index';
|
||||
@ -1,118 +0,0 @@
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { ISwitchCondition, ISwitchNode } from '@/interfaces/database/flow';
|
||||
import { NodeProps, Position } from '@xyflow/react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { NodeHandleId, SwitchOperatorOptions } from '../../constant';
|
||||
import { LogicalOperatorIcon } from '../../form/switch-form';
|
||||
import { useGetVariableLabelByValue } from '../../hooks/use-get-begin-query';
|
||||
import { CommonHandle } from './handle';
|
||||
import { RightHandleStyle } from './handle-icon';
|
||||
import NodeHeader from './node-header';
|
||||
import { NodeWrapper } from './node-wrapper';
|
||||
import { ToolBar } from './toolbar';
|
||||
import { useBuildSwitchHandlePositions } from './use-build-switch-handle-positions';
|
||||
|
||||
const getConditionKey = (idx: number, length: number) => {
|
||||
if (idx === 0 && length !== 1) {
|
||||
return 'If';
|
||||
} else if (idx === length - 1) {
|
||||
return 'Else';
|
||||
}
|
||||
|
||||
return 'ElseIf';
|
||||
};
|
||||
|
||||
const ConditionBlock = ({
|
||||
condition,
|
||||
nodeId,
|
||||
}: { condition: ISwitchCondition } & { nodeId: string }) => {
|
||||
const items = condition?.items ?? [];
|
||||
const getLabel = useGetVariableLabelByValue(nodeId);
|
||||
|
||||
const renderOperatorIcon = useCallback((operator?: string) => {
|
||||
const item = SwitchOperatorOptions.find((x) => x.value === operator);
|
||||
if (item) {
|
||||
return (
|
||||
<LogicalOperatorIcon
|
||||
icon={item?.icon}
|
||||
value={item?.value}
|
||||
></LogicalOperatorIcon>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="p-0 divide-y divide-background-card">
|
||||
{items.map((x, idx) => (
|
||||
<div key={idx}>
|
||||
<section className="flex justify-between gap-2 items-center text-xs p-1">
|
||||
<div className="flex-1 truncate text-accent-primary">
|
||||
{getLabel(x?.cpn_id)}
|
||||
</div>
|
||||
<span>{renderOperatorIcon(x?.operator)}</span>
|
||||
<div className="flex-1 truncate">{x?.value}</div>
|
||||
</section>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
function InnerSwitchNode({ id, data, selected }: NodeProps<ISwitchNode>) {
|
||||
const { positions } = useBuildSwitchHandlePositions({ data, id });
|
||||
return (
|
||||
<ToolBar selected={selected} id={id} label={data.label} showRun={false}>
|
||||
<NodeWrapper selected={selected}>
|
||||
<CommonHandle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
isConnectable
|
||||
nodeId={id}
|
||||
id={NodeHandleId.End}
|
||||
></CommonHandle>
|
||||
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||
<section className="gap-2.5 flex flex-col">
|
||||
{positions.map((position, idx) => {
|
||||
return (
|
||||
<div key={idx}>
|
||||
<section className="flex flex-col text-xs">
|
||||
<div className="text-right">
|
||||
<span>{getConditionKey(idx, positions.length)}</span>
|
||||
<div className="text-text-secondary">
|
||||
{idx < positions.length - 1 && position.text}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-accent-primary">
|
||||
{idx < positions.length - 1 &&
|
||||
position.condition?.logical_operator?.toUpperCase()}
|
||||
</span>
|
||||
{position.condition && (
|
||||
<ConditionBlock
|
||||
condition={position.condition}
|
||||
nodeId={id}
|
||||
></ConditionBlock>
|
||||
)}
|
||||
</section>
|
||||
<CommonHandle
|
||||
key={position.text}
|
||||
id={position.text}
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable
|
||||
style={{ ...RightHandleStyle, top: position.top }}
|
||||
nodeId={id}
|
||||
isConnectableEnd={false}
|
||||
></CommonHandle>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</NodeWrapper>
|
||||
</ToolBar>
|
||||
);
|
||||
}
|
||||
|
||||
export const SwitchNode = memo(InnerSwitchNode);
|
||||
@ -1,78 +0,0 @@
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import { Flex } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { get } from 'lodash';
|
||||
import { useGetComponentLabelByValue } from '../../hooks/use-get-begin-query';
|
||||
import { IGenerateParameter } from '../../interface';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import NodeHeader from './node-header';
|
||||
|
||||
import { ITemplateNode } from '@/interfaces/database/flow';
|
||||
import { memo } from 'react';
|
||||
import styles from './index.less';
|
||||
|
||||
function InnerTemplateNode({
|
||||
id,
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<ITemplateNode>) {
|
||||
const parameters: IGenerateParameter[] = get(data, 'form.parameters', []);
|
||||
const getLabel = useGetComponentLabelByValue(id);
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<section
|
||||
className={classNames(
|
||||
styles.logicNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
|
||||
{
|
||||
[styles.selectedNode]: selected,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Handle
|
||||
id="c"
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
style={LeftHandleStyle}
|
||||
></Handle>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
className={styles.handle}
|
||||
style={RightHandleStyle}
|
||||
id="b"
|
||||
></Handle>
|
||||
|
||||
<NodeHeader
|
||||
id={id}
|
||||
name={data.name}
|
||||
label={data.label}
|
||||
className={styles.nodeHeader}
|
||||
></NodeHeader>
|
||||
|
||||
<Flex gap={8} vertical className={styles.generateParameters}>
|
||||
{parameters.map((x) => (
|
||||
<Flex
|
||||
key={x.id}
|
||||
align="center"
|
||||
gap={6}
|
||||
className={styles.conditionBlock}
|
||||
>
|
||||
<label htmlFor="">{x.key}</label>
|
||||
<span className={styles.parameterValue}>
|
||||
{getLabel(x.component_id)}
|
||||
</span>
|
||||
</Flex>
|
||||
))}
|
||||
</Flex>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export const TemplateNode = memo(InnerTemplateNode);
|
||||
@ -2,9 +2,8 @@ import { IRagNode } from '@/interfaces/database/flow';
|
||||
import { NodeProps, Position } from '@xyflow/react';
|
||||
import { memo } from 'react';
|
||||
import { NodeHandleId } from '../../constant';
|
||||
import { needsSingleStepDebugging } from '../../utils';
|
||||
import { CommonHandle } from './handle';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import { LeftHandleStyle } from './handle-icon';
|
||||
import NodeHeader from './node-header';
|
||||
import { NodeWrapper } from './node-wrapper';
|
||||
import { ToolBar } from './toolbar';
|
||||
@ -20,7 +19,8 @@ function TokenizerNode({
|
||||
selected={selected}
|
||||
id={id}
|
||||
label={data.label}
|
||||
showRun={needsSingleStepDebugging(data.label)}
|
||||
showRun={false}
|
||||
showCopy={false}
|
||||
>
|
||||
<NodeWrapper selected={selected}>
|
||||
<CommonHandle
|
||||
@ -31,15 +31,6 @@ function TokenizerNode({
|
||||
style={LeftHandleStyle}
|
||||
nodeId={id}
|
||||
></CommonHandle>
|
||||
<CommonHandle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
id={NodeHandleId.Start}
|
||||
style={RightHandleStyle}
|
||||
nodeId={id}
|
||||
isConnectableEnd={false}
|
||||
></CommonHandle>
|
||||
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||
</NodeWrapper>
|
||||
</ToolBar>
|
||||
|
||||
@ -1,83 +0,0 @@
|
||||
import { IAgentForm, IToolNode } from '@/interfaces/database/agent';
|
||||
import { Handle, NodeProps, Position } from '@xyflow/react';
|
||||
import { get } from 'lodash';
|
||||
import { MouseEventHandler, memo, useCallback } from 'react';
|
||||
import { NodeHandleId, Operator } from '../../constant';
|
||||
import { ToolCard } from '../../form/agent-form/agent-tools';
|
||||
import { useFindMcpById } from '../../hooks/use-find-mcp-by-id';
|
||||
import OperatorIcon from '../../operator-icon';
|
||||
import useGraphStore from '../../store';
|
||||
import { NodeWrapper } from './node-wrapper';
|
||||
|
||||
function InnerToolNode({
|
||||
id,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<IToolNode>) {
|
||||
const { edges, getNode } = useGraphStore((state) => state);
|
||||
const upstreamAgentNodeId = edges.find((x) => x.target === id)?.source;
|
||||
const upstreamAgentNode = getNode(upstreamAgentNodeId);
|
||||
const { findMcpById } = useFindMcpById();
|
||||
|
||||
const handleClick = useCallback(
|
||||
(operator: string): MouseEventHandler<HTMLLIElement> =>
|
||||
(e) => {
|
||||
if (operator === Operator.Code) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const tools: IAgentForm['tools'] = get(
|
||||
upstreamAgentNode,
|
||||
'data.form.tools',
|
||||
[],
|
||||
);
|
||||
|
||||
const mcpList: IAgentForm['mcp'] = get(
|
||||
upstreamAgentNode,
|
||||
'data.form.mcp',
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<NodeWrapper selected={selected}>
|
||||
<Handle
|
||||
id={NodeHandleId.End}
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
isConnectable={isConnectable}
|
||||
></Handle>
|
||||
<ul className="space-y-2">
|
||||
{tools.map((x) => (
|
||||
<ToolCard
|
||||
key={x.component_name}
|
||||
onClick={handleClick(x.component_name)}
|
||||
className="cursor-pointer"
|
||||
data-tool={x.component_name}
|
||||
>
|
||||
<div className="flex gap-1 items-center pointer-events-none">
|
||||
<OperatorIcon name={x.component_name as Operator}></OperatorIcon>
|
||||
{x.component_name}
|
||||
</div>
|
||||
</ToolCard>
|
||||
))}
|
||||
|
||||
{mcpList.map((x) => (
|
||||
<ToolCard
|
||||
key={x.mcp_id}
|
||||
onClick={handleClick(x.mcp_id)}
|
||||
className="cursor-pointer"
|
||||
data-tool={x.mcp_id}
|
||||
>
|
||||
{findMcpById(x.mcp_id)?.name}
|
||||
</ToolCard>
|
||||
))}
|
||||
</ul>
|
||||
</NodeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export const ToolNode = memo(InnerToolNode);
|
||||
@ -11,7 +11,6 @@ import {
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { Operator } from '../../constant';
|
||||
import { useDuplicateNode } from '../../hooks';
|
||||
import useGraphStore from '../../store';
|
||||
|
||||
@ -28,6 +27,7 @@ type ToolBarProps = {
|
||||
label: string;
|
||||
id: string;
|
||||
showRun?: boolean;
|
||||
showCopy?: boolean;
|
||||
} & PropsWithChildren;
|
||||
|
||||
export function ToolBar({
|
||||
@ -35,23 +35,17 @@ export function ToolBar({
|
||||
children,
|
||||
label,
|
||||
id,
|
||||
showRun = true,
|
||||
showRun = false,
|
||||
showCopy = true,
|
||||
}: ToolBarProps) {
|
||||
const deleteNodeById = useGraphStore((store) => store.deleteNodeById);
|
||||
const deleteIterationNodeById = useGraphStore(
|
||||
(store) => store.deleteIterationNodeById,
|
||||
);
|
||||
|
||||
const deleteNode: MouseEventHandler<HTMLDivElement> = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
if (label === Operator.Iteration) {
|
||||
deleteIterationNodeById(id);
|
||||
} else {
|
||||
deleteNodeById(id);
|
||||
}
|
||||
deleteNodeById(id);
|
||||
},
|
||||
[deleteIterationNodeById, deleteNodeById, id, label],
|
||||
[deleteNodeById, id],
|
||||
);
|
||||
|
||||
const duplicateNode = useDuplicateNode();
|
||||
@ -74,10 +68,13 @@ export function ToolBar({
|
||||
<IconWrapper>
|
||||
<Play className="size-3.5" data-play />
|
||||
</IconWrapper>
|
||||
)}{' '}
|
||||
<IconWrapper onClick={handleDuplicate}>
|
||||
<Copy className="size-3.5" />
|
||||
</IconWrapper>
|
||||
)}
|
||||
{showCopy && (
|
||||
<IconWrapper onClick={handleDuplicate}>
|
||||
<Copy className="size-3.5" />
|
||||
</IconWrapper>
|
||||
)}
|
||||
|
||||
<IconWrapper onClick={deleteNode}>
|
||||
<Trash2 className="size-3.5" />
|
||||
</IconWrapper>
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||
import { useUpdateNodeInternals } from '@xyflow/react';
|
||||
import { get } from 'lodash';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { useCreateCategorizeFormSchema } from '../../form/categorize-form/use-form-schema';
|
||||
|
||||
export const useBuildCategorizeHandlePositions = ({
|
||||
data,
|
||||
id,
|
||||
}: {
|
||||
id: string;
|
||||
data: RAGFlowNodeType['data'];
|
||||
}) => {
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
|
||||
const FormSchema = useCreateCategorizeFormSchema();
|
||||
|
||||
type FormSchemaType = z.infer<typeof FormSchema>;
|
||||
|
||||
const items: Required<FormSchemaType['items']> = useMemo(() => {
|
||||
return get(data, `form.items`, []);
|
||||
}, [data]);
|
||||
|
||||
const positions = useMemo(() => {
|
||||
const list: Array<{
|
||||
top: number;
|
||||
name: string;
|
||||
uuid: string;
|
||||
}> &
|
||||
Required<FormSchemaType['items']> = [];
|
||||
|
||||
items.forEach((x, idx) => {
|
||||
list.push({
|
||||
...x,
|
||||
top: idx === 0 ? 86 : list[idx - 1].top + 8 + 24,
|
||||
});
|
||||
});
|
||||
|
||||
return list;
|
||||
}, [items]);
|
||||
|
||||
useEffect(() => {
|
||||
updateNodeInternals(id);
|
||||
}, [id, updateNodeInternals, items]);
|
||||
|
||||
return { positions };
|
||||
};
|
||||
@ -1,59 +0,0 @@
|
||||
import { ISwitchCondition, RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||
import { useUpdateNodeInternals } from '@xyflow/react';
|
||||
import get from 'lodash/get';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { SwitchElseTo } from '../../constant';
|
||||
import { generateSwitchHandleText } from '../../utils';
|
||||
|
||||
export const useBuildSwitchHandlePositions = ({
|
||||
data,
|
||||
id,
|
||||
}: {
|
||||
id: string;
|
||||
data: RAGFlowNodeType['data'];
|
||||
}) => {
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
|
||||
const conditions: ISwitchCondition[] = useMemo(() => {
|
||||
return get(data, 'form.conditions', []);
|
||||
}, [data]);
|
||||
|
||||
const positions = useMemo(() => {
|
||||
const list: Array<{
|
||||
text: string;
|
||||
top: number;
|
||||
idx: number;
|
||||
condition?: ISwitchCondition;
|
||||
}> = [];
|
||||
|
||||
[...conditions, ''].forEach((x, idx) => {
|
||||
let top = idx === 0 ? 53 : list[idx - 1].top + 10 + 14 + 16 + 16; // case number (Case 1) height + flex gap
|
||||
if (idx >= 1) {
|
||||
const previousItems = conditions[idx - 1]?.items ?? [];
|
||||
if (previousItems.length > 0) {
|
||||
// top += 12; // ConditionBlock padding
|
||||
top += previousItems.length * 26; // condition variable height
|
||||
// top += (previousItems.length - 1) * 25; // operator height
|
||||
}
|
||||
}
|
||||
|
||||
list.push({
|
||||
text:
|
||||
idx < conditions.length
|
||||
? generateSwitchHandleText(idx)
|
||||
: SwitchElseTo,
|
||||
idx,
|
||||
top,
|
||||
condition: typeof x === 'string' ? undefined : x,
|
||||
});
|
||||
});
|
||||
|
||||
return list;
|
||||
}, [conditions]);
|
||||
|
||||
useEffect(() => {
|
||||
updateNodeInternals(id);
|
||||
}, [id, updateNodeInternals, conditions]);
|
||||
|
||||
return { positions };
|
||||
};
|
||||
@ -1,21 +1,10 @@
|
||||
import {
|
||||
initialKeywordsSimilarityWeightValue,
|
||||
initialSimilarityThresholdValue,
|
||||
} from '@/components/similarity-slider';
|
||||
import {
|
||||
AgentGlobals,
|
||||
CodeTemplateStrMap,
|
||||
ProgrammingLanguage,
|
||||
} from '@/constants/agent';
|
||||
|
||||
import { ParseDocumentType } from '@/components/layout-recognize-form-field';
|
||||
import { initialLlmBaseValues } from '@/constants/agent';
|
||||
import {
|
||||
ChatVariableEnabledField,
|
||||
variableEnabledFieldMap,
|
||||
} from '@/constants/chat';
|
||||
import { ModelVariableType } from '@/constants/knowledge';
|
||||
import i18n from '@/locales/config';
|
||||
import { setInitialChatVariableEnabledFieldValue } from '@/utils/chat';
|
||||
import { t } from 'i18next';
|
||||
|
||||
import {
|
||||
Circle,
|
||||
@ -28,6 +17,89 @@ import {
|
||||
WrapText,
|
||||
} from 'lucide-react';
|
||||
|
||||
export enum FileType {
|
||||
PDF = 'pdf',
|
||||
Spreadsheet = 'spreadsheet',
|
||||
Image = 'image',
|
||||
Email = 'email',
|
||||
TextMarkdown = 'text&markdown',
|
||||
Docx = 'word',
|
||||
PowerPoint = 'slides',
|
||||
Video = 'video',
|
||||
Audio = 'audio',
|
||||
}
|
||||
|
||||
export enum PdfOutputFormat {
|
||||
Json = 'json',
|
||||
Markdown = 'markdown',
|
||||
}
|
||||
|
||||
export enum SpreadsheetOutputFormat {
|
||||
Json = 'json',
|
||||
Html = 'html',
|
||||
}
|
||||
|
||||
export enum ImageOutputFormat {
|
||||
Text = 'text',
|
||||
}
|
||||
|
||||
export enum EmailOutputFormat {
|
||||
Json = 'json',
|
||||
Text = 'text',
|
||||
}
|
||||
|
||||
export enum TextMarkdownOutputFormat {
|
||||
Text = 'text',
|
||||
}
|
||||
|
||||
export enum DocxOutputFormat {
|
||||
Markdown = 'markdown',
|
||||
Json = 'json',
|
||||
}
|
||||
|
||||
export enum PptOutputFormat {
|
||||
Json = 'json',
|
||||
}
|
||||
|
||||
export enum VideoOutputFormat {
|
||||
Json = 'json',
|
||||
}
|
||||
|
||||
export enum AudioOutputFormat {
|
||||
Text = 'text',
|
||||
}
|
||||
|
||||
export const OutputFormatMap = {
|
||||
[FileType.PDF]: PdfOutputFormat,
|
||||
[FileType.Spreadsheet]: SpreadsheetOutputFormat,
|
||||
[FileType.Image]: ImageOutputFormat,
|
||||
[FileType.Email]: EmailOutputFormat,
|
||||
[FileType.TextMarkdown]: TextMarkdownOutputFormat,
|
||||
[FileType.Docx]: DocxOutputFormat,
|
||||
[FileType.PowerPoint]: PptOutputFormat,
|
||||
[FileType.Video]: VideoOutputFormat,
|
||||
[FileType.Audio]: AudioOutputFormat,
|
||||
};
|
||||
|
||||
export const InitialOutputFormatMap = {
|
||||
[FileType.PDF]: PdfOutputFormat.Json,
|
||||
[FileType.Spreadsheet]: SpreadsheetOutputFormat.Html,
|
||||
[FileType.Image]: ImageOutputFormat.Text,
|
||||
[FileType.Email]: EmailOutputFormat.Text,
|
||||
[FileType.TextMarkdown]: TextMarkdownOutputFormat.Text,
|
||||
[FileType.Docx]: DocxOutputFormat.Json,
|
||||
[FileType.PowerPoint]: PptOutputFormat.Json,
|
||||
[FileType.Video]: VideoOutputFormat.Json,
|
||||
[FileType.Audio]: AudioOutputFormat.Text,
|
||||
};
|
||||
|
||||
export enum ContextGeneratorFieldName {
|
||||
Summary = 'summary',
|
||||
Keywords = 'keywords',
|
||||
Questions = 'questions',
|
||||
Metadata = 'metadata',
|
||||
}
|
||||
|
||||
export enum PromptRole {
|
||||
User = 'user',
|
||||
Assistant = 'assistant',
|
||||
@ -38,34 +110,16 @@ export enum AgentDialogueMode {
|
||||
Task = 'task',
|
||||
}
|
||||
|
||||
export const BeginId = 'begin';
|
||||
export const BeginId = 'File';
|
||||
|
||||
export enum Operator {
|
||||
Begin = 'Begin',
|
||||
Retrieval = 'Retrieval',
|
||||
Categorize = 'Categorize',
|
||||
Message = 'Message',
|
||||
Relevant = 'Relevant',
|
||||
RewriteQuestion = 'RewriteQuestion',
|
||||
KeywordExtract = 'KeywordExtract',
|
||||
ExeSQL = 'ExeSQL',
|
||||
Switch = 'Switch',
|
||||
Concentrator = 'Concentrator',
|
||||
Begin = 'File',
|
||||
Note = 'Note',
|
||||
Crawler = 'Crawler',
|
||||
Invoke = 'Invoke',
|
||||
Email = 'Email',
|
||||
Iteration = 'Iteration',
|
||||
IterationStart = 'IterationItem',
|
||||
Code = 'CodeExec',
|
||||
WaitingDialogue = 'WaitingDialogue',
|
||||
Agent = 'Agent',
|
||||
Tool = 'Tool',
|
||||
UserFillUp = 'UserFillUp',
|
||||
StringTransform = 'StringTransform',
|
||||
Parser = 'Parser',
|
||||
Chunker = 'Chunker',
|
||||
Tokenizer = 'Tokenizer',
|
||||
Splitter = 'Splitter',
|
||||
HierarchicalMerger = 'HierarchicalMerger',
|
||||
Extractor = 'Extractor',
|
||||
}
|
||||
|
||||
export const SwitchLogicOperatorOptions = ['and', 'or'];
|
||||
@ -74,20 +128,6 @@ export const CommonOperatorList = Object.values(Operator).filter(
|
||||
(x) => x !== Operator.Note,
|
||||
);
|
||||
|
||||
export const AgentOperatorList = [
|
||||
Operator.Retrieval,
|
||||
Operator.Categorize,
|
||||
Operator.Message,
|
||||
Operator.RewriteQuestion,
|
||||
Operator.KeywordExtract,
|
||||
Operator.Switch,
|
||||
Operator.Concentrator,
|
||||
Operator.Iteration,
|
||||
Operator.WaitingDialogue,
|
||||
Operator.Note,
|
||||
Operator.Agent,
|
||||
];
|
||||
|
||||
export const SwitchOperatorOptions = [
|
||||
{ value: '=', label: 'equal', icon: 'equal' },
|
||||
{ value: '≠', label: 'notEqual', icon: 'not-equals' },
|
||||
@ -113,38 +153,45 @@ export const SwitchOperatorOptions = [
|
||||
|
||||
export const SwitchElseTo = 'end_cpn_ids';
|
||||
|
||||
const initialQueryBaseValues = {
|
||||
query: [],
|
||||
};
|
||||
export enum TokenizerSearchMethod {
|
||||
Embedding = 'embedding',
|
||||
FullText = 'full_text',
|
||||
}
|
||||
|
||||
export const initialRetrievalValues = {
|
||||
query: AgentGlobals.SysQuery,
|
||||
top_n: 8,
|
||||
top_k: 1024,
|
||||
kb_ids: [],
|
||||
rerank_id: '',
|
||||
empty_response: '',
|
||||
...initialSimilarityThresholdValue,
|
||||
...initialKeywordsSimilarityWeightValue,
|
||||
use_kg: false,
|
||||
cross_languages: [],
|
||||
export enum ImageParseMethod {
|
||||
OCR = 'ocr',
|
||||
}
|
||||
|
||||
export enum TokenizerFields {
|
||||
Text = 'text',
|
||||
Questions = 'questions',
|
||||
Summary = 'summary',
|
||||
}
|
||||
|
||||
export enum ParserFields {
|
||||
From = 'from',
|
||||
To = 'to',
|
||||
Cc = 'cc',
|
||||
Bcc = 'bcc',
|
||||
Date = 'date',
|
||||
Subject = 'subject',
|
||||
Body = 'body',
|
||||
Attachments = 'attachments',
|
||||
}
|
||||
|
||||
export const initialBeginValues = {
|
||||
outputs: {
|
||||
formalized_content: {
|
||||
name: {
|
||||
type: 'string',
|
||||
value: '',
|
||||
},
|
||||
json: {
|
||||
type: 'Array<Object>',
|
||||
value: [],
|
||||
file: {
|
||||
type: 'Object',
|
||||
value: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const initialBeginValues = {
|
||||
mode: AgentDialogueMode.Conversational,
|
||||
prologue: `Hi! I'm your assistant. What can I do for you?`,
|
||||
};
|
||||
|
||||
export const variableCheckBoxFieldMap = Object.keys(
|
||||
variableEnabledFieldMap,
|
||||
).reduce<Record<string, boolean>>((pre, cur) => {
|
||||
@ -154,215 +201,17 @@ export const variableCheckBoxFieldMap = Object.keys(
|
||||
return pre;
|
||||
}, {});
|
||||
|
||||
const initialLlmBaseValues = {
|
||||
...variableCheckBoxFieldMap,
|
||||
temperature: 0.1,
|
||||
top_p: 0.3,
|
||||
frequency_penalty: 0.7,
|
||||
presence_penalty: 0.4,
|
||||
max_tokens: 256,
|
||||
};
|
||||
|
||||
export const initialGenerateValues = {
|
||||
...initialLlmBaseValues,
|
||||
prompt: i18n.t('flow.promptText'),
|
||||
cite: true,
|
||||
message_history_window_size: 12,
|
||||
parameters: [],
|
||||
};
|
||||
|
||||
export const initialRewriteQuestionValues = {
|
||||
...initialLlmBaseValues,
|
||||
language: '',
|
||||
message_history_window_size: 6,
|
||||
};
|
||||
|
||||
export const initialRelevantValues = {
|
||||
...initialLlmBaseValues,
|
||||
};
|
||||
|
||||
export const initialCategorizeValues = {
|
||||
...initialLlmBaseValues,
|
||||
query: AgentGlobals.SysQuery,
|
||||
parameter: ModelVariableType.Precise,
|
||||
message_history_window_size: 1,
|
||||
items: [],
|
||||
outputs: {
|
||||
category_name: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const initialMessageValues = {
|
||||
content: [''],
|
||||
};
|
||||
|
||||
export const initialKeywordExtractValues = {
|
||||
...initialLlmBaseValues,
|
||||
top_n: 3,
|
||||
...initialQueryBaseValues,
|
||||
};
|
||||
|
||||
export const initialExeSqlValues = {
|
||||
sql: '',
|
||||
db_type: 'mysql',
|
||||
database: '',
|
||||
username: '',
|
||||
host: '',
|
||||
port: 3306,
|
||||
password: '',
|
||||
max_records: 1024,
|
||||
outputs: {
|
||||
formalized_content: {
|
||||
value: '',
|
||||
type: 'string',
|
||||
},
|
||||
json: {
|
||||
value: [],
|
||||
type: 'Array<Object>',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const initialSwitchValues = {
|
||||
conditions: [
|
||||
{
|
||||
logical_operator: SwitchLogicOperatorOptions[0],
|
||||
items: [
|
||||
{
|
||||
operator: SwitchOperatorOptions[0].value,
|
||||
},
|
||||
],
|
||||
to: [],
|
||||
},
|
||||
],
|
||||
[SwitchElseTo]: [],
|
||||
};
|
||||
|
||||
export const initialConcentratorValues = {};
|
||||
|
||||
export const initialNoteValues = {
|
||||
text: '',
|
||||
};
|
||||
|
||||
export const initialCrawlerValues = {
|
||||
extract_type: 'markdown',
|
||||
query: '',
|
||||
};
|
||||
|
||||
export const initialInvokeValues = {
|
||||
url: '',
|
||||
method: 'GET',
|
||||
timeout: 60,
|
||||
headers: `{
|
||||
"Accept": "*/*",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive"
|
||||
}`,
|
||||
proxy: '',
|
||||
clean_html: false,
|
||||
variables: [],
|
||||
outputs: {
|
||||
result: {
|
||||
value: '',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const initialTemplateValues = {
|
||||
content: '',
|
||||
parameters: [],
|
||||
};
|
||||
|
||||
export const initialEmailValues = {
|
||||
smtp_server: '',
|
||||
smtp_port: 465,
|
||||
email: '',
|
||||
password: '',
|
||||
sender_name: '',
|
||||
to_email: '',
|
||||
cc_email: '',
|
||||
subject: '',
|
||||
content: '',
|
||||
outputs: {
|
||||
success: {
|
||||
value: true,
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const initialIterationValues = {
|
||||
items_ref: '',
|
||||
outputs: {},
|
||||
};
|
||||
export const initialIterationStartValues = {
|
||||
outputs: {
|
||||
item: {
|
||||
type: 'unkown',
|
||||
},
|
||||
index: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const initialCodeValues = {
|
||||
lang: ProgrammingLanguage.Python,
|
||||
script: CodeTemplateStrMap[ProgrammingLanguage.Python],
|
||||
arguments: {
|
||||
arg1: '',
|
||||
arg2: '',
|
||||
},
|
||||
outputs: {},
|
||||
};
|
||||
|
||||
export const initialWaitingDialogueValues = {};
|
||||
|
||||
export const initialChunkerValues = { outputs: {} };
|
||||
|
||||
export const initialTokenizerValues = {};
|
||||
|
||||
export const initialAgentValues = {
|
||||
...initialLlmBaseValues,
|
||||
description: '',
|
||||
user_prompt: '',
|
||||
sys_prompt: t('flow.sysPromptDefultValue'),
|
||||
prompts: [{ role: PromptRole.User, content: `{${AgentGlobals.SysQuery}}` }],
|
||||
message_history_window_size: 12,
|
||||
max_retries: 3,
|
||||
delay_after_error: 1,
|
||||
visual_files_var: '',
|
||||
max_rounds: 1,
|
||||
exception_method: '',
|
||||
exception_goto: [],
|
||||
exception_default_value: '',
|
||||
tools: [],
|
||||
mcp: [],
|
||||
cite: true,
|
||||
outputs: {
|
||||
// structured_output: {
|
||||
// topic: {
|
||||
// type: 'string',
|
||||
// description:
|
||||
// 'default:general. The category of the search.news is useful for retrieving real-time updates, particularly about politics, sports, and major current events covered by mainstream media sources. general is for broader, more general-purpose searches that may include a wide range of sources.',
|
||||
// enum: ['general', 'news'],
|
||||
// default: 'general',
|
||||
// },
|
||||
// },
|
||||
content: {
|
||||
type: 'string',
|
||||
value: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const initialUserFillUpValues = {
|
||||
enable_tips: true,
|
||||
tips: '',
|
||||
inputs: [],
|
||||
export const initialTokenizerValues = {
|
||||
search_method: [
|
||||
TokenizerSearchMethod.Embedding,
|
||||
TokenizerSearchMethod.FullText,
|
||||
],
|
||||
filename_embd_weight: 0.1,
|
||||
fields: TokenizerFields.Text,
|
||||
outputs: {},
|
||||
};
|
||||
|
||||
@ -380,19 +229,84 @@ export enum StringTransformDelimiter {
|
||||
Space = ' ',
|
||||
}
|
||||
|
||||
export const initialStringTransformValues = {
|
||||
method: StringTransformMethod.Merge,
|
||||
split_ref: '',
|
||||
script: '',
|
||||
delimiters: [StringTransformDelimiter.Comma],
|
||||
export const initialParserValues = {
|
||||
outputs: {
|
||||
result: {
|
||||
type: 'string',
|
||||
},
|
||||
markdown: { type: 'string', value: '' },
|
||||
text: { type: 'string', value: '' },
|
||||
html: { type: 'string', value: '' },
|
||||
json: { type: 'Array<object>', value: [] },
|
||||
},
|
||||
setups: [
|
||||
{
|
||||
fileFormat: FileType.PDF,
|
||||
output_format: PdfOutputFormat.Json,
|
||||
parse_method: ParseDocumentType.DeepDOC,
|
||||
},
|
||||
{
|
||||
fileFormat: FileType.Spreadsheet,
|
||||
output_format: SpreadsheetOutputFormat.Html,
|
||||
},
|
||||
{
|
||||
fileFormat: FileType.Image,
|
||||
output_format: ImageOutputFormat.Text,
|
||||
parse_method: ImageParseMethod.OCR,
|
||||
system_prompt: '',
|
||||
},
|
||||
{
|
||||
fileFormat: FileType.Email,
|
||||
fields: Object.values(ParserFields),
|
||||
output_format: EmailOutputFormat.Text,
|
||||
},
|
||||
{
|
||||
fileFormat: FileType.TextMarkdown,
|
||||
output_format: TextMarkdownOutputFormat.Text,
|
||||
},
|
||||
{
|
||||
fileFormat: FileType.Docx,
|
||||
output_format: DocxOutputFormat.Json,
|
||||
},
|
||||
{
|
||||
fileFormat: FileType.PowerPoint,
|
||||
output_format: PptOutputFormat.Json,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const initialParserValues = { outputs: {} };
|
||||
export const initialSplitterValues = {
|
||||
outputs: {
|
||||
chunks: { type: 'Array<Object>', value: [] },
|
||||
},
|
||||
chunk_token_size: 512,
|
||||
overlapped_percent: 0,
|
||||
delimiters: [{ value: '\n' }],
|
||||
};
|
||||
|
||||
export enum Hierarchy {
|
||||
H1 = '1',
|
||||
H2 = '2',
|
||||
H3 = '3',
|
||||
H4 = '4',
|
||||
H5 = '5',
|
||||
}
|
||||
|
||||
export const initialHierarchicalMergerValues = {
|
||||
outputs: {
|
||||
chunks: { type: 'Array<Object>', value: [] },
|
||||
},
|
||||
hierarchy: Hierarchy.H3,
|
||||
levels: [
|
||||
{ expressions: [{ expression: '^#[^#]' }] },
|
||||
{ expressions: [{ expression: '^##[^#]' }] },
|
||||
{ expressions: [{ expression: '^###[^#]' }] },
|
||||
{ expressions: [{ expression: '^####[^#]' }] },
|
||||
],
|
||||
};
|
||||
|
||||
export const initialExtractorValues = {
|
||||
...initialLlmBaseValues,
|
||||
field_name: ContextGeneratorFieldName.Summary,
|
||||
outputs: {},
|
||||
};
|
||||
|
||||
export const CategorizeAnchorPointPositions = [
|
||||
{ top: 1, right: 34 },
|
||||
@ -411,72 +325,24 @@ export const CategorizeAnchorPointPositions = [
|
||||
|
||||
// key is the source of the edge, value is the target of the edge
|
||||
// no connection lines are allowed between key and value
|
||||
export const RestrictedUpstreamMap = {
|
||||
[Operator.Begin]: [Operator.Relevant],
|
||||
[Operator.Categorize]: [Operator.Begin, Operator.Categorize],
|
||||
[Operator.Retrieval]: [Operator.Begin, Operator.Retrieval],
|
||||
[Operator.Message]: [
|
||||
Operator.Begin,
|
||||
Operator.Message,
|
||||
Operator.Retrieval,
|
||||
Operator.RewriteQuestion,
|
||||
Operator.Categorize,
|
||||
],
|
||||
[Operator.Relevant]: [Operator.Begin],
|
||||
[Operator.RewriteQuestion]: [
|
||||
Operator.Begin,
|
||||
Operator.Message,
|
||||
Operator.RewriteQuestion,
|
||||
Operator.Relevant,
|
||||
],
|
||||
[Operator.KeywordExtract]: [
|
||||
Operator.Begin,
|
||||
Operator.Message,
|
||||
Operator.Relevant,
|
||||
],
|
||||
[Operator.ExeSQL]: [Operator.Begin],
|
||||
[Operator.Switch]: [Operator.Begin],
|
||||
[Operator.Concentrator]: [Operator.Begin],
|
||||
[Operator.Crawler]: [Operator.Begin],
|
||||
[Operator.Note]: [],
|
||||
[Operator.Invoke]: [Operator.Begin],
|
||||
[Operator.Email]: [Operator.Begin],
|
||||
[Operator.Iteration]: [Operator.Begin],
|
||||
[Operator.IterationStart]: [Operator.Begin],
|
||||
[Operator.Code]: [Operator.Begin],
|
||||
[Operator.WaitingDialogue]: [Operator.Begin],
|
||||
[Operator.Agent]: [Operator.Begin],
|
||||
[Operator.StringTransform]: [Operator.Begin],
|
||||
[Operator.UserFillUp]: [Operator.Begin],
|
||||
[Operator.Tool]: [Operator.Begin],
|
||||
export const RestrictedUpstreamMap: Record<Operator, Operator[]> = {
|
||||
[Operator.Begin]: [] as Operator[],
|
||||
[Operator.Parser]: [Operator.Begin],
|
||||
[Operator.Splitter]: [Operator.Begin],
|
||||
[Operator.HierarchicalMerger]: [Operator.Begin],
|
||||
[Operator.Tokenizer]: [Operator.Begin],
|
||||
[Operator.Extractor]: [Operator.Begin],
|
||||
[Operator.Note]: [Operator.Begin],
|
||||
};
|
||||
|
||||
export const NodeMap = {
|
||||
[Operator.Begin]: 'beginNode',
|
||||
[Operator.Categorize]: 'categorizeNode',
|
||||
[Operator.Retrieval]: 'retrievalNode',
|
||||
[Operator.Message]: 'messageNode',
|
||||
[Operator.Relevant]: 'relevantNode',
|
||||
[Operator.RewriteQuestion]: 'rewriteNode',
|
||||
[Operator.KeywordExtract]: 'keywordNode',
|
||||
[Operator.ExeSQL]: 'ragNode',
|
||||
[Operator.Switch]: 'switchNode',
|
||||
[Operator.Concentrator]: 'logicNode',
|
||||
[Operator.Note]: 'noteNode',
|
||||
[Operator.Crawler]: 'ragNode',
|
||||
[Operator.Invoke]: 'ragNode',
|
||||
[Operator.Email]: 'ragNode',
|
||||
[Operator.Iteration]: 'group',
|
||||
[Operator.IterationStart]: 'iterationStartNode',
|
||||
[Operator.Code]: 'ragNode',
|
||||
[Operator.WaitingDialogue]: 'ragNode',
|
||||
[Operator.Agent]: 'agentNode',
|
||||
[Operator.Tool]: 'toolNode',
|
||||
[Operator.UserFillUp]: 'ragNode',
|
||||
[Operator.StringTransform]: 'ragNode',
|
||||
[Operator.Parser]: 'parserNode',
|
||||
[Operator.Chunker]: 'chunkerNode',
|
||||
[Operator.Tokenizer]: 'tokenizerNode',
|
||||
[Operator.Splitter]: 'splitterNode',
|
||||
[Operator.HierarchicalMerger]: 'hierarchicalMergerNode',
|
||||
[Operator.Extractor]: 'contextNode',
|
||||
};
|
||||
|
||||
export enum BeginQueryType {
|
||||
@ -497,16 +363,7 @@ export const BeginQueryTypeIconMap = {
|
||||
[BeginQueryType.Boolean]: ToggleLeft,
|
||||
};
|
||||
|
||||
export const NoDebugOperatorsList = [
|
||||
Operator.Begin,
|
||||
Operator.Concentrator,
|
||||
Operator.Message,
|
||||
Operator.RewriteQuestion,
|
||||
Operator.Switch,
|
||||
Operator.Iteration,
|
||||
Operator.UserFillUp,
|
||||
Operator.IterationStart,
|
||||
];
|
||||
export const NoDebugOperatorsList = [Operator.Begin];
|
||||
|
||||
export enum NodeHandleId {
|
||||
Start = 'start',
|
||||
@ -527,3 +384,38 @@ export enum AgentExceptionMethod {
|
||||
Comment = 'comment',
|
||||
Goto = 'goto',
|
||||
}
|
||||
|
||||
export const FileTypeSuffixMap = {
|
||||
[FileType.PDF]: ['pdf'],
|
||||
[FileType.Spreadsheet]: ['xls', 'xlsx', 'csv'],
|
||||
[FileType.Image]: ['jpg', 'jpeg', 'png', 'gif'],
|
||||
[FileType.Email]: ['eml', 'msg'],
|
||||
[FileType.TextMarkdown]: ['md', 'markdown', 'mdx', 'txt'],
|
||||
[FileType.Docx]: ['doc', 'docx'],
|
||||
[FileType.PowerPoint]: ['pptx'],
|
||||
[FileType.Video]: [],
|
||||
[FileType.Audio]: [
|
||||
'da',
|
||||
'wave',
|
||||
'wav',
|
||||
'mp3',
|
||||
'aac',
|
||||
'flac',
|
||||
'ogg',
|
||||
'aiff',
|
||||
'au',
|
||||
'midi',
|
||||
'wma',
|
||||
'realaudio',
|
||||
'vqf',
|
||||
'oggvorbis',
|
||||
'ape',
|
||||
],
|
||||
};
|
||||
|
||||
export const SingleOperators = [
|
||||
Operator.Tokenizer,
|
||||
Operator.Splitter,
|
||||
Operator.HierarchicalMerger,
|
||||
Operator.Parser,
|
||||
];
|
||||
|
||||
@ -48,3 +48,11 @@ export type HandleContextType = {
|
||||
export const HandleContext = createContext<HandleContextType>(
|
||||
{} as HandleContextType,
|
||||
);
|
||||
|
||||
export type LogContextType = {
|
||||
messageId: string;
|
||||
setMessageId: (messageId: string) => void;
|
||||
setUploadedFileData: (data: Record<string, any>) => void;
|
||||
};
|
||||
|
||||
export const LogContext = createContext<LogContextType>({} as LogContextType);
|
||||
|
||||
@ -1,97 +1,30 @@
|
||||
import { Operator } from '../constant';
|
||||
import AgentForm from '../form/agent-form';
|
||||
import BeginForm from '../form/begin-form';
|
||||
import CategorizeForm from '../form/categorize-form';
|
||||
import ChunkerForm from '../form/chunker-form';
|
||||
import CodeForm from '../form/code-form';
|
||||
import CrawlerForm from '../form/crawler-form';
|
||||
import EmailForm from '../form/email-form';
|
||||
import ExeSQLForm from '../form/exesql-form';
|
||||
import InvokeForm from '../form/invoke-form';
|
||||
import IterationForm from '../form/iteration-form';
|
||||
import IterationStartForm from '../form/iteration-start-from';
|
||||
import KeywordExtractForm from '../form/keyword-extract-form';
|
||||
import MessageForm from '../form/message-form';
|
||||
import ExtractorForm from '../form/extractor-form';
|
||||
import HierarchicalMergerForm from '../form/hierarchical-merger-form';
|
||||
import ParserForm from '../form/parser-form';
|
||||
import RelevantForm from '../form/relevant-form';
|
||||
import RetrievalForm from '../form/retrieval-form/next';
|
||||
import RewriteQuestionForm from '../form/rewrite-question-form';
|
||||
import StringTransformForm from '../form/string-transform-form';
|
||||
import SwitchForm from '../form/switch-form';
|
||||
import SplitterForm from '../form/splitter-form';
|
||||
import TokenizerForm from '../form/tokenizer-form';
|
||||
import UserFillUpForm from '../form/user-fill-up-form';
|
||||
|
||||
export const FormConfigMap = {
|
||||
[Operator.Begin]: {
|
||||
component: BeginForm,
|
||||
},
|
||||
[Operator.Retrieval]: {
|
||||
component: RetrievalForm,
|
||||
},
|
||||
[Operator.Categorize]: {
|
||||
component: CategorizeForm,
|
||||
},
|
||||
[Operator.Message]: {
|
||||
component: MessageForm,
|
||||
},
|
||||
[Operator.Relevant]: {
|
||||
component: RelevantForm,
|
||||
},
|
||||
[Operator.RewriteQuestion]: {
|
||||
component: RewriteQuestionForm,
|
||||
},
|
||||
[Operator.Code]: {
|
||||
component: CodeForm,
|
||||
},
|
||||
[Operator.WaitingDialogue]: {
|
||||
component: CodeForm,
|
||||
},
|
||||
[Operator.Agent]: {
|
||||
component: AgentForm,
|
||||
},
|
||||
[Operator.KeywordExtract]: {
|
||||
component: KeywordExtractForm,
|
||||
},
|
||||
[Operator.ExeSQL]: {
|
||||
component: ExeSQLForm,
|
||||
},
|
||||
[Operator.Switch]: {
|
||||
component: SwitchForm,
|
||||
},
|
||||
[Operator.Crawler]: {
|
||||
component: CrawlerForm,
|
||||
},
|
||||
[Operator.Invoke]: {
|
||||
component: InvokeForm,
|
||||
},
|
||||
[Operator.Concentrator]: {
|
||||
component: () => <></>,
|
||||
},
|
||||
[Operator.Note]: {
|
||||
component: () => <></>,
|
||||
},
|
||||
[Operator.Email]: {
|
||||
component: EmailForm,
|
||||
},
|
||||
[Operator.Iteration]: {
|
||||
component: IterationForm,
|
||||
},
|
||||
[Operator.IterationStart]: {
|
||||
component: IterationStartForm,
|
||||
},
|
||||
[Operator.UserFillUp]: {
|
||||
component: UserFillUpForm,
|
||||
},
|
||||
[Operator.StringTransform]: {
|
||||
component: StringTransformForm,
|
||||
},
|
||||
[Operator.Parser]: {
|
||||
component: ParserForm,
|
||||
},
|
||||
[Operator.Chunker]: {
|
||||
component: ChunkerForm,
|
||||
},
|
||||
[Operator.Tokenizer]: {
|
||||
component: TokenizerForm,
|
||||
},
|
||||
[Operator.Splitter]: {
|
||||
component: SplitterForm,
|
||||
},
|
||||
[Operator.HierarchicalMerger]: {
|
||||
component: HierarchicalMergerForm,
|
||||
},
|
||||
[Operator.Extractor]: {
|
||||
component: ExtractorForm,
|
||||
},
|
||||
};
|
||||
|
||||
@ -8,19 +8,13 @@ import {
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/flow';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { lowerFirst } from 'lodash';
|
||||
import { Play, X } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BeginId, Operator } from '../constant';
|
||||
import { AgentFormContext } from '../context';
|
||||
import { RunTooltip } from '../flow-tooltip';
|
||||
import { useHandleNodeNameChange } from '../hooks/use-change-node-name';
|
||||
import OperatorIcon from '../operator-icon';
|
||||
import useGraphStore from '../store';
|
||||
import { needsSingleStepDebugging } from '../utils';
|
||||
import { FormConfigMap } from './form-config-map';
|
||||
import SingleDebugSheet from './single-debug-sheet';
|
||||
|
||||
interface IProps {
|
||||
node?: RAGFlowNodeType;
|
||||
@ -36,13 +30,10 @@ const FormSheet = ({
|
||||
visible,
|
||||
hideModal,
|
||||
node,
|
||||
singleDebugDrawerVisible,
|
||||
chatVisible,
|
||||
hideSingleDebugDrawer,
|
||||
showSingleDebugDrawer,
|
||||
}: IModalProps<any> & IProps) => {
|
||||
const operatorName: Operator = node?.data.label as Operator;
|
||||
const clickedToolId = useGraphStore((state) => state.clickedToolId);
|
||||
// const clickedToolId = useGraphStore((state) => state.clickedToolId);
|
||||
|
||||
const currentFormMap = FormConfigMap[operatorName];
|
||||
|
||||
@ -53,13 +44,6 @@ const FormSheet = ({
|
||||
data: node?.data,
|
||||
});
|
||||
|
||||
const isMcp = useMemo(() => {
|
||||
return (
|
||||
operatorName === Operator.Tool &&
|
||||
Object.values(Operator).every((x) => x !== clickedToolId)
|
||||
);
|
||||
}, [clickedToolId, operatorName]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@ -75,41 +59,28 @@ const FormSheet = ({
|
||||
<section className="flex-col border-b py-2 px-5">
|
||||
<div className="flex items-center gap-2 pb-3">
|
||||
<OperatorIcon name={operatorName}></OperatorIcon>
|
||||
|
||||
{isMcp ? (
|
||||
<div className="flex-1">MCP Config</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-1 flex-1">
|
||||
<label htmlFor="">{t('flow.title')}</label>
|
||||
{node?.id === BeginId ? (
|
||||
<span>{t(BeginId)}</span>
|
||||
) : (
|
||||
<Input
|
||||
value={name}
|
||||
onBlur={handleNameBlur}
|
||||
onChange={handleNameChange}
|
||||
></Input>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{needsSingleStepDebugging(operatorName) && (
|
||||
<div className="flex items-center gap-1 flex-1">
|
||||
<label htmlFor="">{t('flow.title')}</label>
|
||||
{node?.id === BeginId ? (
|
||||
<span>{t(BeginId)}</span>
|
||||
) : (
|
||||
<Input
|
||||
value={name}
|
||||
onBlur={handleNameBlur}
|
||||
onChange={handleNameChange}
|
||||
></Input>
|
||||
)}
|
||||
</div>
|
||||
{/* {needsSingleStepDebugging(operatorName) && (
|
||||
<RunTooltip>
|
||||
<Play
|
||||
className="size-5 cursor-pointer"
|
||||
onClick={showSingleDebugDrawer}
|
||||
/>
|
||||
</RunTooltip>
|
||||
)}
|
||||
)} */}
|
||||
<X onClick={hideModal} />
|
||||
</div>
|
||||
{isMcp || (
|
||||
<span>
|
||||
{t(
|
||||
`dataflow.${lowerFirst(operatorName === Operator.Tool ? clickedToolId : operatorName)}Description`,
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</section>
|
||||
</SheetHeader>
|
||||
<section className="pt-4 overflow-auto flex-1">
|
||||
@ -120,13 +91,6 @@ const FormSheet = ({
|
||||
)}
|
||||
</section>
|
||||
</SheetContent>
|
||||
{singleDebugDrawerVisible && (
|
||||
<SingleDebugSheet
|
||||
visible={singleDebugDrawerVisible}
|
||||
hideModal={hideSingleDebugDrawer}
|
||||
componentId={node?.id}
|
||||
></SingleDebugSheet>
|
||||
)}
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,89 +0,0 @@
|
||||
import CopyToClipboard from '@/components/copy-to-clipboard';
|
||||
import { Sheet, SheetContent, SheetHeader } from '@/components/ui/sheet';
|
||||
import { useDebugSingle, useFetchInputForm } from '@/hooks/use-agent-request';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { X } from 'lucide-react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import JsonView from 'react18-json-view';
|
||||
import 'react18-json-view/src/style.css';
|
||||
import DebugContent from '../../debug-content';
|
||||
import { transferInputsArrayToObject } from '../../form/begin-form/use-watch-change';
|
||||
import { buildBeginInputListFromObject } from '../../form/begin-form/utils';
|
||||
|
||||
interface IProps {
|
||||
componentId?: string;
|
||||
}
|
||||
|
||||
const SingleDebugSheet = ({
|
||||
componentId,
|
||||
visible,
|
||||
hideModal,
|
||||
}: IModalProps<any> & IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const inputForm = useFetchInputForm(componentId);
|
||||
const { debugSingle, data, loading } = useDebugSingle();
|
||||
|
||||
const list = useMemo(() => {
|
||||
return buildBeginInputListFromObject(inputForm);
|
||||
}, [inputForm]);
|
||||
|
||||
const onOk = useCallback(
|
||||
(nextValues: any[]) => {
|
||||
if (componentId) {
|
||||
debugSingle({
|
||||
component_id: componentId,
|
||||
params: transferInputsArrayToObject(nextValues),
|
||||
});
|
||||
}
|
||||
},
|
||||
[componentId, debugSingle],
|
||||
);
|
||||
|
||||
const content = JSON.stringify(data, null, 2);
|
||||
|
||||
return (
|
||||
<Sheet open={visible} modal={false}>
|
||||
<SheetContent className="top-20 p-0" closeIcon={false}>
|
||||
<SheetHeader className="py-2 px-5">
|
||||
<div className="flex justify-between ">
|
||||
{t('flow.testRun')}
|
||||
<X onClick={hideModal} className="cursor-pointer" />
|
||||
</div>
|
||||
</SheetHeader>
|
||||
<section className="overflow-y-auto pt-4 px-5">
|
||||
<DebugContent
|
||||
parameters={list}
|
||||
ok={onOk}
|
||||
isNext={false}
|
||||
loading={loading}
|
||||
submitButtonDisabled={list.length === 0}
|
||||
></DebugContent>
|
||||
{!isEmpty(data) ? (
|
||||
<div
|
||||
className={cn('mt-4 rounded-md border', {
|
||||
[`border-state-error`]: !isEmpty(data._ERROR),
|
||||
})}
|
||||
>
|
||||
<div className="flex justify-between p-2">
|
||||
<span>JSON</span>
|
||||
<CopyToClipboard text={content}></CopyToClipboard>
|
||||
</div>
|
||||
<JsonView
|
||||
src={data}
|
||||
displaySize
|
||||
collapseStringsAfterLength={100000000000}
|
||||
className="w-full h-[800px] break-words overflow-auto p-2"
|
||||
dark
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
||||
export default SingleDebugSheet;
|
||||
@ -1,191 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@ -1,93 +0,0 @@
|
||||
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);
|
||||
@ -1,63 +0,0 @@
|
||||
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);
|
||||
@ -1,280 +0,0 @@
|
||||
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);
|
||||
@ -1,89 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@ -1,178 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
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 };
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
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 };
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
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 };
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
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]);
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@ -1,205 +0,0 @@
|
||||
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);
|
||||
@ -1,226 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@ -1,199 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
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,
|
||||
};
|
||||
};
|
||||
@ -1,34 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
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]);
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
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;
|
||||
},
|
||||
[],
|
||||
);
|
||||
}
|
||||
@ -1,249 +0,0 @@
|
||||
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);
|
||||
@ -1,68 +0,0 @@
|
||||
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);
|
||||
@ -1,48 +0,0 @@
|
||||
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);
|
||||
@ -1,32 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
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]);
|
||||
}
|
||||
@ -1,140 +0,0 @@
|
||||
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 { memo } from 'react';
|
||||
import { useForm, useFormContext } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { initialChunkerValues } 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(initialChunkerValues.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 ChunkerForm = ({ node }: INextOperatorForm) => {
|
||||
const { t } = useTranslate('flow');
|
||||
const defaultValues = useFormValues(initialChunkerValues, 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 memo(ChunkerForm);
|
||||
@ -1,168 +0,0 @@
|
||||
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);
|
||||
@ -1,128 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
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>;
|
||||
@ -1,47 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,95 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1,127 +0,0 @@
|
||||
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;
|
||||
@ -1,135 +0,0 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export const ProgrammaticTag = 'programmatic';
|
||||
@ -1,76 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
@ -1,181 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
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 };
|
||||
@ -1,43 +0,0 @@
|
||||
/**
|
||||
* 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',
|
||||
},
|
||||
};
|
||||
@ -1,91 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@ -1,297 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1,66 +0,0 @@
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1,105 +0,0 @@
|
||||
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);
|
||||
@ -1,161 +0,0 @@
|
||||
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;
|
||||
@ -1,167 +0,0 @@
|
||||
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);
|
||||
@ -1,31 +0,0 @@
|
||||
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 };
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user