mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 12:32:30 +08:00
### What problem does this PR solve? Feat: Add note node #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
58
web/package-lock.json
generated
58
web/package-lock.json
generated
@ -70,6 +70,7 @@
|
||||
"mammoth": "^1.7.2",
|
||||
"next-themes": "^0.4.6",
|
||||
"openai-speech-stream-player": "^1.0.8",
|
||||
"pptx-preview": "^1.0.5",
|
||||
"rc-tween-one": "^3.0.6",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dropzone": "^14.3.5",
|
||||
@ -15819,6 +15820,22 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.6.0.tgz",
|
||||
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0",
|
||||
"zrender": "5.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts/node_modules/tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@ -26241,6 +26258,32 @@
|
||||
"resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
|
||||
},
|
||||
"node_modules/pptx-preview": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/pptx-preview/-/pptx-preview-1.0.5.tgz",
|
||||
"integrity": "sha512-4SafvnLUpwpAY9pHHTHzzU77DANTnxZQgnLK51g3qqv0CMSOAV6f9SVc9ANYXJ0+vyTwakt780xY4s/mbRO/KQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"echarts": "^5.5.1",
|
||||
"jszip": "^3.10.1",
|
||||
"lodash": "^4.17.21",
|
||||
"tslib": "^2.7.0",
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pptx-preview/node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@ -33925,6 +33968,21 @@
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.6.1.tgz",
|
||||
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zrender/node_modules/tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-4.5.2.tgz",
|
||||
|
||||
@ -318,7 +318,7 @@ export const useFetchMessageTrace = () => {
|
||||
refetchOnWindowFocus: false,
|
||||
gcTime: 0,
|
||||
enabled: !!id && !!messageId,
|
||||
refetchInterval: 2000,
|
||||
refetchInterval: 3000,
|
||||
queryFn: async () => {
|
||||
const { data } = await flowService.trace({
|
||||
canvas_id: id,
|
||||
|
||||
@ -1,11 +1,21 @@
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
Background,
|
||||
ConnectionMode,
|
||||
ControlButton,
|
||||
Controls,
|
||||
NodeTypes,
|
||||
ReactFlow,
|
||||
} from '@xyflow/react';
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { useEffect } from 'react';
|
||||
import { NotebookPen } from 'lucide-react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChatSheet } from '../chat/chat-sheet';
|
||||
import {
|
||||
AgentChatContext,
|
||||
@ -21,6 +31,7 @@ import {
|
||||
import { useAddNode } from '../hooks/use-add-node';
|
||||
import { useBeforeDelete } from '../hooks/use-before-delete';
|
||||
import { useCacheChatLog } from '../hooks/use-cache-chat-log';
|
||||
import { useMoveNote } from '../hooks/use-move-note';
|
||||
import { useShowDrawer, useShowLogSheet } from '../hooks/use-show-drawer';
|
||||
import { LogSheet } from '../log-sheet';
|
||||
import RunSheet from '../run-sheet';
|
||||
@ -77,6 +88,7 @@ interface IProps {
|
||||
}
|
||||
|
||||
function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
@ -94,7 +106,6 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
|
||||
const {
|
||||
onNodeClick,
|
||||
onPaneClick,
|
||||
clickedNode,
|
||||
formDrawerVisible,
|
||||
hideFormDrawer,
|
||||
@ -124,7 +135,17 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
|
||||
const { handleBeforeDelete } = useBeforeDelete();
|
||||
|
||||
const { addCanvasNode } = useAddNode(reactFlowInstance);
|
||||
const { addCanvasNode, addNoteNode } = useAddNode(reactFlowInstance);
|
||||
|
||||
const { ref, showImage, hideImage, imgVisible, mouse } = useMoveNote();
|
||||
|
||||
const onPaneClick = useCallback(() => {
|
||||
hideFormDrawer();
|
||||
if (imgVisible) {
|
||||
addNoteNode(mouse);
|
||||
hideImage();
|
||||
}
|
||||
}, [addNoteNode, hideFormDrawer, hideImage, imgVisible, mouse]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chatVisible) {
|
||||
@ -176,6 +197,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
onEdgeMouseEnter={onEdgeMouseEnter}
|
||||
onEdgeMouseLeave={onEdgeMouseLeave}
|
||||
className="h-full"
|
||||
colorMode="dark"
|
||||
defaultEdgeOptions={{
|
||||
type: 'buttonEdge',
|
||||
markerEnd: 'logo',
|
||||
@ -189,8 +211,22 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
|
||||
onBeforeDelete={handleBeforeDelete}
|
||||
>
|
||||
<Background />
|
||||
<Controls position={'bottom-center'} orientation="horizontal">
|
||||
<ControlButton>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<NotebookPen className="!fill-none" onClick={showImage} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('flow.note')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</ControlButton>
|
||||
</Controls>
|
||||
</ReactFlow>
|
||||
</AgentInstanceContext.Provider>
|
||||
<NotebookPen
|
||||
className={cn('hidden absolute size-6', { block: imgVisible })}
|
||||
ref={ref}
|
||||
></NotebookPen>
|
||||
{formDrawerVisible && (
|
||||
<AgentInstanceContext.Provider value={{ addCanvasNode }}>
|
||||
<FormSheet
|
||||
|
||||
@ -12,41 +12,9 @@ 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';
|
||||
|
||||
function ResizeIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="#5025f9"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
}}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<polyline points="16 20 20 20 20 16" />
|
||||
<line x1="14" y1="14" x2="20" y2="20" />
|
||||
<polyline points="8 4 4 4 4 8" />
|
||||
<line x1="4" y1="4" x2="10" y2="10" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
const controlStyle = {
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
cursor: 'nwse-resize',
|
||||
};
|
||||
|
||||
export function InnerIterationNode({
|
||||
id,
|
||||
data,
|
||||
|
||||
@ -1,90 +0,0 @@
|
||||
import { NodeProps, NodeResizeControl } from '@xyflow/react';
|
||||
import { Flex, Form, Input } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import NodeDropdown from './dropdown';
|
||||
|
||||
import SvgIcon from '@/components/svg-icon';
|
||||
import { useTheme } from '@/components/theme-provider';
|
||||
import { INoteNode } from '@/interfaces/database/flow';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHandleNodeNameChange } from '../../hooks';
|
||||
import { useHandleFormValuesChange } from '../../hooks/use-watch-form-change';
|
||||
import styles from './index.less';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const controlStyle = {
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
};
|
||||
|
||||
function NoteNode({ data, id }: NodeProps<INoteNode>) {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm();
|
||||
const { theme } = useTheme();
|
||||
|
||||
const { name, handleNameBlur, handleNameChange } = useHandleNodeNameChange({
|
||||
id,
|
||||
data,
|
||||
});
|
||||
const { handleValuesChange } = useHandleFormValuesChange(id);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(data?.form);
|
||||
}, [form, data?.form]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NodeResizeControl style={controlStyle} minWidth={190} minHeight={128}>
|
||||
<SvgIcon
|
||||
name="resize"
|
||||
width={12}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
cursor: 'nwse-resize',
|
||||
}}
|
||||
></SvgIcon>
|
||||
</NodeResizeControl>
|
||||
<section
|
||||
className={classNames(
|
||||
styles.noteNode,
|
||||
theme === 'dark' ? styles.dark : '',
|
||||
)}
|
||||
>
|
||||
<Flex
|
||||
justify={'space-between'}
|
||||
className={classNames('note-drag-handle')}
|
||||
align="center"
|
||||
gap={6}
|
||||
>
|
||||
<SvgIcon name="note" width={14}></SvgIcon>
|
||||
<Input
|
||||
value={name ?? t('flow.note')}
|
||||
onBlur={handleNameBlur}
|
||||
onChange={handleNameChange}
|
||||
className={styles.noteName}
|
||||
></Input>
|
||||
<NodeDropdown id={id} label={data.label}></NodeDropdown>
|
||||
</Flex>
|
||||
<Form
|
||||
onValuesChange={handleValuesChange}
|
||||
form={form}
|
||||
className={styles.noteForm}
|
||||
>
|
||||
<Form.Item name="text" noStyle>
|
||||
<TextArea
|
||||
rows={3}
|
||||
placeholder={t('flow.notePlaceholder')}
|
||||
className={styles.noteTextarea}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(NoteNode);
|
||||
76
web/src/pages/agent/canvas/node/note-node/index.tsx
Normal file
76
web/src/pages/agent/canvas/node/note-node/index.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { NodeProps, NodeResizeControl } from '@xyflow/react';
|
||||
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { INoteNode } from '@/interfaces/database/flow';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { NotebookPen } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
import { NodeWrapper } from '../node-wrapper';
|
||||
import { ResizeIcon, controlStyle } from '../resize-icon';
|
||||
import { useChangeName, useWatchFormChange } from './use-watch-change';
|
||||
|
||||
const FormSchema = z.object({
|
||||
text: z.string(),
|
||||
});
|
||||
|
||||
function NoteNode({ data, id }: NodeProps<INoteNode>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: data.form,
|
||||
});
|
||||
|
||||
const { handleChangeName } = useChangeName(id);
|
||||
|
||||
useWatchFormChange(id, form);
|
||||
|
||||
return (
|
||||
<NodeWrapper className="p-0 w-full h-full flex flex-col rounded-md ">
|
||||
<NodeResizeControl minWidth={190} minHeight={128} style={controlStyle}>
|
||||
<ResizeIcon />
|
||||
</NodeResizeControl>
|
||||
<section className="px-1 py-2 flex gap-2 bg-background-highlight items-center note-drag-handle rounded-s-md">
|
||||
<NotebookPen className="size-4" />
|
||||
<Input
|
||||
type="text"
|
||||
defaultValue={data.name}
|
||||
onChange={handleChangeName}
|
||||
></Input>
|
||||
</section>
|
||||
<Form {...form}>
|
||||
<form className="flex-1">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="text"
|
||||
render={({ field }) => (
|
||||
<FormItem className="h-full">
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder={t('flow.notePlaceholder')}
|
||||
className="resize-none rounded-none p-1 h-full overflow-auto bg-background-header-bar"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
</NodeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(NoteNode);
|
||||
@ -0,0 +1,31 @@
|
||||
import useGraphStore from '@/pages/agent/store';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { UseFormReturn, useWatch } from 'react-hook-form';
|
||||
|
||||
export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) {
|
||||
let values = useWatch({ control: form?.control });
|
||||
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
|
||||
|
||||
useEffect(() => {
|
||||
// Manually triggered form updates are synchronized to the canvas
|
||||
if (id) {
|
||||
values = form?.getValues() || {};
|
||||
let nextValues: any = values;
|
||||
|
||||
updateNodeForm(id, nextValues);
|
||||
}
|
||||
}, [id, updateNodeForm, values]);
|
||||
}
|
||||
|
||||
export function useChangeName(id: string) {
|
||||
const updateNodeName = useGraphStore((state) => state.updateNodeName);
|
||||
|
||||
const handleChangeName = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateNodeName(id, e.target.value.trim());
|
||||
},
|
||||
[id, updateNodeName],
|
||||
);
|
||||
|
||||
return { handleChangeName };
|
||||
}
|
||||
32
web/src/pages/agent/canvas/node/resize-icon.tsx
Normal file
32
web/src/pages/agent/canvas/node/resize-icon.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
export function ResizeIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="2"
|
||||
stroke="#5025f9"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
}}
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<polyline points="16 20 20 20 20 16" />
|
||||
<line x1="14" y1="14" x2="20" y2="20" />
|
||||
<polyline points="8 4 4 4 4 8" />
|
||||
<line x1="4" y1="4" x2="10" y2="10" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export const controlStyle = {
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
cursor: 'nwse-resize',
|
||||
};
|
||||
@ -85,6 +85,10 @@ function findInputFromList(eventList: IEventList) {
|
||||
};
|
||||
}
|
||||
|
||||
export function getLatestError(eventList: IEventList) {
|
||||
return get(eventList.at(-1), 'data.outputs._ERROR');
|
||||
}
|
||||
|
||||
const useGetBeginNodePrologue = () => {
|
||||
const getNode = useGraphStore((state) => state.getNode);
|
||||
|
||||
@ -159,7 +163,7 @@ export const useSendNextMessage = () => {
|
||||
const inputAnswer = findInputFromList(answerList);
|
||||
if (answerList.length > 0) {
|
||||
addNewestOneAnswer({
|
||||
answer: content,
|
||||
answer: content || getLatestError(answerList),
|
||||
id: id,
|
||||
...inputAnswer,
|
||||
});
|
||||
|
||||
@ -76,7 +76,7 @@ export function FileUploadDirectUpload({
|
||||
onUpload={onUpload}
|
||||
onFileReject={onFileReject}
|
||||
maxFiles={1}
|
||||
className="w-full max-w-md"
|
||||
className="w-full"
|
||||
multiple={false}
|
||||
>
|
||||
<FileUploadDropzone>
|
||||
|
||||
@ -269,9 +269,13 @@ function useResizeIterationNode() {
|
||||
|
||||
return { resizeIterationNode };
|
||||
}
|
||||
type CanvasMouseEvent = Pick<
|
||||
React.MouseEvent<HTMLElement>,
|
||||
'clientX' | 'clientY'
|
||||
>;
|
||||
|
||||
export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
|
||||
const { edges, nodes, addEdge, addNode, getNode, updateNode } = useGraphStore(
|
||||
const { edges, nodes, addEdge, addNode, getNode } = useGraphStore(
|
||||
(state) => state,
|
||||
);
|
||||
const getNodeName = useGetNodeName();
|
||||
@ -290,7 +294,7 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
|
||||
position: Position.Right,
|
||||
},
|
||||
) =>
|
||||
(event?: React.MouseEvent<HTMLElement>) => {
|
||||
(event?: CanvasMouseEvent) => {
|
||||
const nodeId = params.nodeId;
|
||||
|
||||
const node = getNode(nodeId);
|
||||
@ -303,7 +307,7 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
|
||||
y: event?.clientY || 0,
|
||||
});
|
||||
|
||||
if (params.position === Position.Right) {
|
||||
if (params.position === Position.Right && type !== Operator.Note) {
|
||||
position = calculateNewlyBackChildPosition(nodeId, params.id);
|
||||
}
|
||||
|
||||
@ -420,9 +424,16 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
|
||||
initializeOperatorParams,
|
||||
nodes,
|
||||
reactFlowInstance,
|
||||
updateNode,
|
||||
resizeIterationNode,
|
||||
],
|
||||
);
|
||||
|
||||
return { addCanvasNode };
|
||||
const addNoteNode = useCallback(
|
||||
(e: CanvasMouseEvent) => {
|
||||
addCanvasNode(Operator.Note)(e);
|
||||
},
|
||||
[addCanvasNode],
|
||||
);
|
||||
|
||||
return { addCanvasNode, addNoteNode };
|
||||
}
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { IEventList, MessageEventType } from '@/hooks/use-send-message';
|
||||
import {
|
||||
IEventList,
|
||||
INodeEvent,
|
||||
MessageEventType,
|
||||
} from '@/hooks/use-send-message';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
export const ExcludeTypes = [
|
||||
@ -41,11 +45,13 @@ export function useCacheChatLog() {
|
||||
}, []);
|
||||
|
||||
const currentEventListWithoutMessage = useMemo(() => {
|
||||
return eventList.filter(
|
||||
const list = eventList.filter(
|
||||
(x) =>
|
||||
x.message_id === currentMessageId &&
|
||||
ExcludeTypes.every((y) => y !== x.event),
|
||||
);
|
||||
|
||||
return list as INodeEvent[];
|
||||
}, [currentMessageId, eventList]);
|
||||
|
||||
return {
|
||||
|
||||
35
web/src/pages/agent/hooks/use-move-note.ts
Normal file
35
web/src/pages/agent/hooks/use-move-note.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { useMouse } from 'ahooks';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export function useMoveNote() {
|
||||
const ref = useRef<SVGSVGElement>(null);
|
||||
const mouse = useMouse();
|
||||
const [imgVisible, setImgVisible] = useState(false);
|
||||
|
||||
const toggleVisible = useCallback((visible: boolean) => {
|
||||
setImgVisible(visible);
|
||||
}, []);
|
||||
|
||||
const showImage = useCallback(() => {
|
||||
toggleVisible(true);
|
||||
}, [toggleVisible]);
|
||||
|
||||
const hideImage = useCallback(() => {
|
||||
toggleVisible(false);
|
||||
}, [toggleVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.style.top = `${mouse.clientY - 70}px`;
|
||||
ref.current.style.left = `${mouse.clientX + 10}px`;
|
||||
}
|
||||
}, [mouse.clientX, mouse.clientY]);
|
||||
|
||||
return {
|
||||
ref,
|
||||
showImage,
|
||||
hideImage,
|
||||
mouse,
|
||||
imgVisible,
|
||||
};
|
||||
}
|
||||
@ -1,154 +1,6 @@
|
||||
import { CodeTemplateStrMap, ProgrammingLanguage } from '@/constants/agent';
|
||||
import { settledModelVariableMap } from '@/constants/knowledge';
|
||||
import { omit } from 'lodash';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { UseFormReturn, useWatch } from 'react-hook-form';
|
||||
import { Operator } from '../constant';
|
||||
import useGraphStore from '../store';
|
||||
import { buildCategorizeObjectFromList, convertToStringArray } from '../utils';
|
||||
|
||||
export const useHandleFormValuesChange = (
|
||||
operatorName: Operator,
|
||||
id?: string,
|
||||
form?: UseFormReturn,
|
||||
) => {
|
||||
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
|
||||
|
||||
const handleValuesChange = useCallback(
|
||||
(changedValues: any, values: any) => {
|
||||
let nextValues: any = values;
|
||||
// Fixed the issue that the related form value does not change after selecting the freedom field of the model
|
||||
if (
|
||||
Object.keys(changedValues).length === 1 &&
|
||||
'parameter' in changedValues &&
|
||||
changedValues['parameter'] in settledModelVariableMap
|
||||
) {
|
||||
nextValues = {
|
||||
...values,
|
||||
...settledModelVariableMap[
|
||||
changedValues['parameter'] as keyof typeof settledModelVariableMap
|
||||
],
|
||||
};
|
||||
}
|
||||
if (id) {
|
||||
updateNodeForm(id, nextValues);
|
||||
}
|
||||
},
|
||||
[updateNodeForm, id],
|
||||
);
|
||||
|
||||
let values = useWatch({ control: form?.control });
|
||||
|
||||
// console.log('🚀 ~ x:', values);
|
||||
|
||||
useEffect(() => {
|
||||
// Manually triggered form updates are synchronized to the canvas
|
||||
if (id && form?.formState.isDirty) {
|
||||
values = form?.getValues();
|
||||
let nextValues: any = values;
|
||||
// run(id, nextValues);
|
||||
|
||||
const categoryDescriptionRegex = /items\.\d+\.name/g;
|
||||
|
||||
if (operatorName === Operator.Categorize) {
|
||||
console.log('🚀 ~ useEffect ~ values:', values);
|
||||
const categoryDescription = Array.isArray(values.items)
|
||||
? buildCategorizeObjectFromList(values.items)
|
||||
: {};
|
||||
if (categoryDescription) {
|
||||
nextValues = {
|
||||
...omit(values, 'items'),
|
||||
category_description: categoryDescription,
|
||||
};
|
||||
}
|
||||
} else if (operatorName === Operator.Message) {
|
||||
nextValues = {
|
||||
...values,
|
||||
content: convertToStringArray(values.content),
|
||||
};
|
||||
}
|
||||
|
||||
updateNodeForm(id, nextValues);
|
||||
}
|
||||
}, [form?.formState.isDirty, id, operatorName, updateNodeForm, values]);
|
||||
|
||||
// useEffect(() => {
|
||||
// form?.subscribe({
|
||||
// formState: { values: true },
|
||||
// callback: ({ values }) => {
|
||||
// // console.info('subscribe', values);
|
||||
// },
|
||||
// });
|
||||
// }, [form]);
|
||||
|
||||
return { handleValuesChange };
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = form?.watch((value, { name, type, values }) => {
|
||||
if (id && name) {
|
||||
let nextValues: any = value;
|
||||
|
||||
// Fixed the issue that the related form value does not change after selecting the freedom field of the model
|
||||
if (
|
||||
name === 'parameter' &&
|
||||
value['parameter'] in settledModelVariableMap
|
||||
) {
|
||||
nextValues = {
|
||||
...value,
|
||||
...settledModelVariableMap[
|
||||
value['parameter'] as keyof typeof settledModelVariableMap
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const categoryDescriptionRegex = /items\.\d+\.name/g;
|
||||
if (
|
||||
operatorName === Operator.Categorize &&
|
||||
categoryDescriptionRegex.test(name)
|
||||
) {
|
||||
nextValues = {
|
||||
...omit(value, 'items'),
|
||||
category_description: buildCategorizeObjectFromList(value.items),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
operatorName === Operator.Code &&
|
||||
type === 'change' &&
|
||||
name === 'lang'
|
||||
) {
|
||||
nextValues = {
|
||||
...value,
|
||||
script: CodeTemplateStrMap[value.lang as ProgrammingLanguage],
|
||||
};
|
||||
}
|
||||
|
||||
if (operatorName === Operator.Message) {
|
||||
nextValues = {
|
||||
...value,
|
||||
content: convertToStringArray(value.content),
|
||||
};
|
||||
}
|
||||
|
||||
// Manually triggered form updates are synchronized to the canvas
|
||||
if (form.formState.isDirty) {
|
||||
console.log(
|
||||
'🚀 ~ useEffect ~ value:',
|
||||
name,
|
||||
type,
|
||||
values,
|
||||
operatorName,
|
||||
);
|
||||
// run(id, nextValues);
|
||||
updateNodeForm(id, nextValues);
|
||||
}
|
||||
}
|
||||
});
|
||||
return () => subscription?.unsubscribe();
|
||||
}, [form, form?.watch, id, operatorName, updateNodeForm]);
|
||||
|
||||
return { handleValuesChange };
|
||||
};
|
||||
|
||||
export function useWatchFormChange(id?: string, form?: UseFormReturn) {
|
||||
let values = useWatch({ control: form?.control });
|
||||
|
||||
@ -19,11 +19,15 @@ import {
|
||||
SheetTitle,
|
||||
} from '@/components/ui/sheet';
|
||||
import { useFetchMessageTrace } from '@/hooks/use-agent-request';
|
||||
import { ILogEvent, MessageEventType } from '@/hooks/use-send-message';
|
||||
import {
|
||||
INodeData,
|
||||
INodeEvent,
|
||||
MessageEventType,
|
||||
} from '@/hooks/use-send-message';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { ITraceData } from '@/interfaces/database/agent';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { get } from 'lodash';
|
||||
import { BellElectric, NotebookText } from 'lucide-react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import JsonView from 'react18-json-view';
|
||||
@ -57,25 +61,19 @@ function JsonViewer({
|
||||
);
|
||||
}
|
||||
|
||||
function concatData(
|
||||
firstRecord: Record<string, any> | Array<Record<string, any>>,
|
||||
nextRecord: Record<string, any> | Array<Record<string, any>>,
|
||||
function getInputsOrOutputs(
|
||||
nodeEventList: INodeData[],
|
||||
field: 'inputs' | 'outputs',
|
||||
) {
|
||||
let result: Array<Record<string, any>> = [];
|
||||
const inputsOrOutputs = nodeEventList.map((x) => get(x, field, {}));
|
||||
|
||||
if (!isEmpty(firstRecord)) {
|
||||
result = result.concat(firstRecord);
|
||||
if (inputsOrOutputs.length < 2) {
|
||||
return inputsOrOutputs[0] || {};
|
||||
}
|
||||
|
||||
if (!isEmpty(nextRecord)) {
|
||||
result = result.concat(nextRecord);
|
||||
}
|
||||
|
||||
return isEmpty(result) ? {} : result;
|
||||
return inputsOrOutputs;
|
||||
}
|
||||
|
||||
type EventWithIndex = { startNodeIdx: number } & ILogEvent;
|
||||
|
||||
export function LogSheet({
|
||||
hideModal,
|
||||
currentEventListWithoutMessage,
|
||||
@ -96,68 +94,58 @@ export function LogSheet({
|
||||
[getNode],
|
||||
);
|
||||
|
||||
const startedNodeList = useMemo(() => {
|
||||
const duplicateList = currentEventListWithoutMessage.filter(
|
||||
(x) => x.event === MessageEventType.NodeStarted,
|
||||
) as INodeEvent[];
|
||||
|
||||
// Remove duplicate nodes
|
||||
return duplicateList.reduce<Array<INodeEvent>>((pre, cur) => {
|
||||
if (pre.every((x) => x.data.component_id !== cur.data.component_id)) {
|
||||
pre.push(cur);
|
||||
}
|
||||
return pre;
|
||||
}, []);
|
||||
}, [currentEventListWithoutMessage]);
|
||||
|
||||
const hasTrace = useCallback(
|
||||
(componentId: string) => {
|
||||
if (Array.isArray(traceData)) {
|
||||
return traceData?.some((x) => x.component_id === componentId);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[traceData],
|
||||
);
|
||||
|
||||
const filterTrace = useCallback(
|
||||
(componentId: string) => {
|
||||
return traceData
|
||||
const trace = traceData
|
||||
?.filter((x) => x.component_id === componentId)
|
||||
.reduce<ITraceData['trace']>((pre, cur) => {
|
||||
pre.push(...cur.trace);
|
||||
|
||||
return pre;
|
||||
}, []);
|
||||
return Array.isArray(trace) ? trace : {};
|
||||
},
|
||||
[traceData],
|
||||
);
|
||||
|
||||
// Look up to find the nearest start component id and concatenate the finish and log data into one
|
||||
const finishedNodeList = useMemo(() => {
|
||||
return currentEventListWithoutMessage.filter(
|
||||
(x) =>
|
||||
x.event === MessageEventType.NodeFinished ||
|
||||
x.event === MessageEventType.NodeLogs,
|
||||
) as ILogEvent[];
|
||||
}, [currentEventListWithoutMessage]);
|
||||
const filterFinishedNodeList = useCallback(
|
||||
(componentId: string) => {
|
||||
const nodeEventList = currentEventListWithoutMessage
|
||||
.filter(
|
||||
(x) =>
|
||||
x.event === MessageEventType.NodeFinished &&
|
||||
(x.data as INodeData)?.component_id === componentId,
|
||||
)
|
||||
.map((x) => x.data);
|
||||
|
||||
const nextList = useMemo(() => {
|
||||
return finishedNodeList.reduce<Array<EventWithIndex>>((pre, cur) => {
|
||||
const startNodeIdx = (
|
||||
currentEventListWithoutMessage as Array<ILogEvent>
|
||||
).findLastIndex(
|
||||
(x) =>
|
||||
x.data.component_id === cur.data.component_id &&
|
||||
x.event === MessageEventType.NodeStarted,
|
||||
);
|
||||
|
||||
const item = pre.find((x) => x.startNodeIdx === startNodeIdx);
|
||||
|
||||
const { inputs = {}, outputs = {} } = cur.data;
|
||||
if (item) {
|
||||
const { inputs: inputList, outputs: outputList } = item.data;
|
||||
|
||||
item.data = {
|
||||
...item.data,
|
||||
inputs: concatData(inputList, inputs),
|
||||
outputs: concatData(outputList, outputs),
|
||||
};
|
||||
} else {
|
||||
pre.push({
|
||||
...cur,
|
||||
startNodeIdx,
|
||||
});
|
||||
}
|
||||
|
||||
return pre;
|
||||
}, []);
|
||||
}, [currentEventListWithoutMessage, finishedNodeList]);
|
||||
return nodeEventList;
|
||||
},
|
||||
[currentEventListWithoutMessage],
|
||||
);
|
||||
|
||||
return (
|
||||
<Sheet open onOpenChange={hideModal} modal={false}>
|
||||
@ -170,76 +158,75 @@ export function LogSheet({
|
||||
</SheetHeader>
|
||||
<section className="max-h-[82vh] overflow-auto mt-6">
|
||||
<Timeline>
|
||||
{nextList.map((x, idx) => (
|
||||
<TimelineItem
|
||||
key={idx}
|
||||
step={idx}
|
||||
className="group-data-[orientation=vertical]/timeline:ms-10 group-data-[orientation=vertical]/timeline:not-last:pb-8"
|
||||
>
|
||||
<TimelineHeader>
|
||||
<TimelineSeparator className="group-data-[orientation=vertical]/timeline:-left-7 group-data-[orientation=vertical]/timeline:h-[calc(100%-1.5rem-0.25rem)] group-data-[orientation=vertical]/timeline:translate-y-6.5 top-6 bg-background-checked" />
|
||||
{startedNodeList.map((x, idx) => {
|
||||
const nodeDataList = filterFinishedNodeList(x.data.component_id);
|
||||
const inputs = getInputsOrOutputs(nodeDataList, 'inputs');
|
||||
const outputs = getInputsOrOutputs(nodeDataList, 'outputs');
|
||||
return (
|
||||
<TimelineItem
|
||||
key={idx}
|
||||
step={idx}
|
||||
className="group-data-[orientation=vertical]/timeline:ms-10 group-data-[orientation=vertical]/timeline:not-last:pb-8"
|
||||
>
|
||||
<TimelineHeader>
|
||||
<TimelineSeparator className="group-data-[orientation=vertical]/timeline:-left-7 group-data-[orientation=vertical]/timeline:h-[calc(100%-1.5rem-0.25rem)] group-data-[orientation=vertical]/timeline:translate-y-6.5 top-6 bg-background-checked" />
|
||||
|
||||
<TimelineIndicator className="bg-primary/10 group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground flex size-6 items-center justify-center border-none group-data-[orientation=vertical]/timeline:-left-7">
|
||||
<BellElectric className="size-5" />
|
||||
{/* <img
|
||||
src={item.image}
|
||||
alt={item.title}
|
||||
className="size-6 rounded-full"
|
||||
/> */}
|
||||
</TimelineIndicator>
|
||||
</TimelineHeader>
|
||||
<TimelineContent className="text-foreground rounded-lg border mb-5">
|
||||
<section key={idx}>
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="bg-background-card px-3"
|
||||
>
|
||||
<AccordionItem value={idx.toString()}>
|
||||
<AccordionTrigger>
|
||||
<div className="flex gap-2 items-center">
|
||||
<span>{getNodeName(x.data?.component_id)}</span>
|
||||
<span className="text-text-sub-title text-xs">
|
||||
{x.data.elapsed_time?.toString().slice(0, 6)}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'border-background -end-1 -top-1 size-2 rounded-full border-2 bg-dot-green',
|
||||
{ 'text-dot-green': x.data.error === null },
|
||||
{ 'text-dot-red': x.data.error !== null },
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Online</span>
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-2">
|
||||
<JsonViewer
|
||||
data={x.data.inputs}
|
||||
title="Input"
|
||||
></JsonViewer>
|
||||
|
||||
{hasTrace(x.data.component_id) && (
|
||||
<TimelineIndicator className="bg-primary/10 group-data-completed/timeline-item:bg-primary group-data-completed/timeline-item:text-primary-foreground flex size-6 items-center justify-center border-none group-data-[orientation=vertical]/timeline:-left-7">
|
||||
<BellElectric className="size-5" />
|
||||
</TimelineIndicator>
|
||||
</TimelineHeader>
|
||||
<TimelineContent className="text-foreground rounded-lg border mb-5">
|
||||
<section key={idx}>
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="bg-background-card px-3"
|
||||
>
|
||||
<AccordionItem value={idx.toString()}>
|
||||
<AccordionTrigger>
|
||||
<div className="flex gap-2 items-center">
|
||||
<span>{getNodeName(x.data?.component_id)}</span>
|
||||
<span className="text-text-sub-title text-xs">
|
||||
{x.data.elapsed_time?.toString().slice(0, 6)}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'border-background -end-1 -top-1 size-2 rounded-full border-2 bg-dot-green',
|
||||
{ 'text-dot-green': x.data.error === null },
|
||||
{ 'text-dot-red': x.data.error !== null },
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Online</span>
|
||||
</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-2">
|
||||
<JsonViewer
|
||||
data={filterTrace(x.data.component_id) ?? {}}
|
||||
title={'Trace'}
|
||||
data={inputs}
|
||||
title="Input"
|
||||
></JsonViewer>
|
||||
)}
|
||||
|
||||
<JsonViewer
|
||||
data={x.data.outputs}
|
||||
title={'Output'}
|
||||
></JsonViewer>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</section>
|
||||
{/* <TimelineDate className="mt-1 mb-0">{item.date}</TimelineDate> */}
|
||||
</TimelineContent>
|
||||
</TimelineItem>
|
||||
))}
|
||||
{hasTrace(x.data.component_id) && (
|
||||
<JsonViewer
|
||||
data={filterTrace(x.data.component_id)}
|
||||
title={'Trace'}
|
||||
></JsonViewer>
|
||||
)}
|
||||
|
||||
<JsonViewer
|
||||
data={outputs}
|
||||
title={'Output'}
|
||||
></JsonViewer>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</section>
|
||||
</TimelineContent>
|
||||
</TimelineItem>
|
||||
);
|
||||
})}
|
||||
</Timeline>
|
||||
</section>
|
||||
</SheetContent>
|
||||
|
||||
@ -21,6 +21,7 @@ export const OperatorIconMap = {
|
||||
[Operator.Agent]: 'agent-ai',
|
||||
[Operator.UserFillUp]: 'await',
|
||||
[Operator.StringTransform]: 'a-textprocessing',
|
||||
[Operator.Note]: 'notebook-pen',
|
||||
// [Operator.Relevant]: BranchesOutlined,
|
||||
// [Operator.RewriteQuestion]: FormOutlined,
|
||||
// [Operator.KeywordExtract]: KeywordIcon,
|
||||
|
||||
@ -47,7 +47,7 @@ export const currentReg = /\[ID:(\d+)\]/g;
|
||||
|
||||
// To be compatible with the old index matching mode
|
||||
export const replaceTextByOldReg = (text: string) => {
|
||||
return text.replace(oldReg, (substring: string) => {
|
||||
return text?.replace(oldReg, (substring: string) => {
|
||||
return `[ID:${substring.slice(2, -2)}]`;
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user