support flowgram
22
package.json
|
|
@ -51,7 +51,10 @@
|
|||
}
|
||||
},
|
||||
"overrides": {
|
||||
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3"
|
||||
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
|
||||
"@codemirror/state": "^6",
|
||||
"@codemirror/view": "^6",
|
||||
"@codemirror/language": "^6"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
@ -105,17 +108,12 @@
|
|||
"@ant-design/pro-layout": "6.38.22",
|
||||
"@codemirror/state": "^6.4.1",
|
||||
"@codemirror/view": "^6.34.1",
|
||||
"@flowgram.ai/free-container-plugin": "1.0.2",
|
||||
"@flowgram.ai/free-group-plugin": "1.0.2",
|
||||
"@flowgram.ai/free-layout-editor": "1.0.2",
|
||||
"@flowgram.ai/free-lines-plugin": "1.0.2",
|
||||
"@flowgram.ai/free-node-panel-plugin": "1.0.2",
|
||||
"@flowgram.ai/free-snap-plugin": "1.0.2",
|
||||
"@flowgram.ai/free-stack-plugin": "1.0.2",
|
||||
"@flowgram.ai/history-node-plugin": "1.0.2",
|
||||
"@flowgram.ai/fixed-layout-editor": "1.0.2",
|
||||
"@flowgram.ai/form-materials": "1.0.2",
|
||||
"@flowgram.ai/fixed-semi-materials": "1.0.2",
|
||||
"@flowgram.ai/group-plugin": "1.0.2",
|
||||
"@flowgram.ai/minimap-plugin": "1.0.2",
|
||||
"@flowgram.ai/panel-manager-plugin": "1.0.2",
|
||||
"@flowgram.ai/runtime-interface": "1.0.2",
|
||||
"@monaco-editor/react": "4.2.1",
|
||||
"@react-hook/resize-observer": "^2.0.2",
|
||||
"@types/body-parser": "^1.19.2",
|
||||
|
|
@ -156,6 +154,7 @@
|
|||
"axios": "^1.4.0",
|
||||
"compression-webpack-plugin": "9.2.0",
|
||||
"concurrently": "^7.0.0",
|
||||
"classnames": "^2.5.1",
|
||||
"file-saver": "2.0.2",
|
||||
"lint-staged": "^13.0.3",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
|
@ -181,6 +180,7 @@
|
|||
"react-router-dom": "6.26.1",
|
||||
"react-split-pane": "^0.1.92",
|
||||
"sockjs-client": "^1.6.0",
|
||||
"styled-components": "^5.3.10",
|
||||
"ts-node": "^10.9.2",
|
||||
"ts-proto": "^2.6.1",
|
||||
"tslib": "^2.4.0",
|
||||
|
|
@ -188,4 +188,4 @@
|
|||
"vh-check": "^2.0.5",
|
||||
"virtualizedtableforantd4": "1.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
3687
pnpm-lock.yaml
|
|
@ -39,6 +39,7 @@ body {
|
|||
max-height: calc(80vh - 110px);
|
||||
max-height: calc(80vh - var(--vh-offset, 110px));
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.log-modal {
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
import React, { useEffect, useImperativeHandle, forwardRef } from 'react';
|
||||
import { EditorRenderer, FreeLayoutEditorProvider } from '@flowgram.ai/free-layout-editor';
|
||||
import { DockedPanelLayer } from '@flowgram.ai/panel-manager-plugin';
|
||||
import '@flowgram.ai/free-layout-editor/index.css';
|
||||
import { nodeRegistries } from './nodes';
|
||||
import { useEditorProps } from './hooks/use-editor-props';
|
||||
import './editor.less';
|
||||
|
||||
export interface FlowgramEditorProps {
|
||||
initialData?: any;
|
||||
onChange?: (data: any) => void;
|
||||
}
|
||||
|
||||
export interface FlowgramEditorRef {
|
||||
getData: () => any;
|
||||
}
|
||||
|
||||
const FlowgramEditor = forwardRef<FlowgramEditorRef, FlowgramEditorProps>(
|
||||
({ initialData, onChange }, ref) => {
|
||||
const defaultData = initialData || {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
};
|
||||
|
||||
const editorProps = useEditorProps(defaultData, nodeRegistries);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getData: () => {
|
||||
// This would need to be implemented to get the current editor state
|
||||
// For now, return the default data structure
|
||||
return defaultData;
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
// Setup change listener
|
||||
// This would need integration with Flowgram's onChange events
|
||||
}
|
||||
}, [onChange]);
|
||||
|
||||
return (
|
||||
<div className="flowgram-editor-container">
|
||||
<FreeLayoutEditorProvider {...editorProps}>
|
||||
<div className="flowgram-editor-wrapper">
|
||||
<EditorRenderer className="flowgram-editor" />
|
||||
<DockedPanelLayer />
|
||||
</div>
|
||||
</FreeLayoutEditorProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FlowgramEditor.displayName = 'FlowgramEditor';
|
||||
|
||||
export default FlowgramEditor;
|
||||
1
src/pages/scenario/flowgram/assets/icon-break.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" focusable="false" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M9.56066 2.43934C10.1464 3.02513 10.1464 3.97487 9.56066 4.56066L7.12132 7H14.75C18.8353 7 22 10.5796 22 14.5C22 18.4204 18.8353 22 14.75 22H11.5C10.6716 22 10 21.3284 10 20.5C10 19.6716 10.6716 19 11.5 19H14.75C17.016 19 19 16.9308 19 14.5C19 12.0692 17.016 10 14.75 10H7.12132L9.56066 12.4393C10.1464 13.0251 10.1464 13.9749 9.56066 14.5607C8.97487 15.1464 8.02513 15.1464 7.43934 14.5607L2.43934 9.56066C1.85355 8.97487 1.85355 8.02513 2.43934 7.43934L7.43934 2.43934C8.02513 1.85355 8.97487 1.85355 9.56066 2.43934Z" fill="#54A9FF"></path></svg>
|
||||
|
After Width: | Height: | Size: 733 B |
BIN
src/pages/scenario/flowgram/assets/icon-case.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
9
src/pages/scenario/flowgram/assets/icon-condition.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="44" height="45" viewBox="0 0 44 45" fill="none" class="injected-svg" data-src="https://lf3-static.bytednsdoc.com/obj/eden-cn/uvpahtvabh_lm_zhhwh/ljhwZthlaukjlkulzlp/activity_icons/exclusive-split-0518.svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.4705 14.0152C15.299 12.8436 15.299 10.944 16.4705 9.77244L20.7131 5.5297C21.8846 4.3581 23.784 4.3581 24.9556 5.5297L29.1981 9.77244C30.3697 10.944 30.3697 12.8436 29.1981 14.0152L25.1206 18.0929H32.6674C36.5334 18.0929 39.6674 21.2269 39.6674 25.0929V33.154V33.3271V37.154C39.6674 38.2585 38.7719 39.154 37.6674 39.154H33.6674C32.5628 39.154 31.6674 38.2585 31.6674 37.154V33.3271V33.154V26.0929H23.5948H15.6674V33.1327L17.2685 33.1244C18.8397 33.1163 19.6322 35.0156 18.5212 36.1266L12.7374 41.9103C12.0506 42.5971 10.9371 42.5971 10.2503 41.9103L4.52588 36.1859C3.42107 35.0811 4.19797 33.1917 5.76038 33.1837L7.66737 33.1739V25.0929C7.66737 21.227 10.8014 18.0929 14.6674 18.0929H20.5481L16.4705 14.0152Z" fill="url(#paint0_linear_2752_183702-7)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2752_183702-7" x1="38.52" y1="43.3915" x2="8.09686" y2="4.6982" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3370FF"/>
|
||||
<stop offset="0.997908" stop-color="#33A9FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/pages/scenario/flowgram/assets/icon-end.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src/pages/scenario/flowgram/assets/icon-if.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
src/pages/scenario/flowgram/assets/icon-llm.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
9
src/pages/scenario/flowgram/assets/icon-loop.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 44 44" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.1112 5.9556C31.0632 4.90759 29.2716 5.65513 29.2792 7.13722L29.2873 8.71503H12.1184C8.37321 8.71503 5.33711 11.7511 5.33711 15.4963H5.34238C5.33971 15.538 5.33839 15.58 5.33846 15.6224L5.34892 21.6473C5.35171 23.2499 7.22508 24.1194 8.4509 23.087L12.2732 19.8679C12.9121 19.3298 13.2806 18.5369 13.2801 17.7016L13.2795 16.715H29.3285L29.3351 17.9931C29.3427 19.4669 31.125 20.1998 32.1671 19.1576L37.5671 13.7576C38.215 13.1098 38.215 12.0594 37.5671 11.4115L32.1112 5.9556ZM13.279 15.8694L13.2788 15.6243C13.2788 15.5813 13.2773 15.5386 13.2745 15.4963H13.3371C13.3371 15.6265 13.3167 15.7518 13.279 15.8694ZM11.4759 37.9731C12.5239 39.0211 14.3156 38.2736 14.3079 36.7915L14.2998 35.2137H31.4687C35.2139 35.2137 38.25 32.1776 38.25 28.4324H38.2447C38.2474 28.3907 38.2487 28.3487 38.2487 28.3063L38.2382 22.2814C38.2354 20.6788 36.362 19.8093 35.1362 20.8417L31.314 24.0608C30.675 24.599 30.3065 25.3918 30.307 26.2272L30.3076 27.2137H14.2586L14.252 25.9356C14.2444 24.4618 12.4622 23.7289 11.42 24.7711L6.02002 30.1711C5.37215 30.819 5.37215 31.8694 6.02002 32.5172L11.4759 37.9731ZM30.3082 28.0593L30.3083 28.3044C30.3083 28.3474 30.3098 28.3901 30.3127 28.4324H30.25C30.25 28.3023 30.2704 28.1769 30.3082 28.0593Z" fill="url(#paint0_linear_775_1137)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_775_1137" x1="6.39609" y1="39.3063" x2="32.5905" y2="4.10488" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3370FF"/>
|
||||
<stop offset="0.997908" stop-color="#33A9FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
11
src/pages/scenario/flowgram/assets/icon-memory.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
width="44" height="44">
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_memory" x1="1024" y1="1024" x2="8.09686" y2="4.6982" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3370FF"/>
|
||||
<stop offset="0.997908" stop-color="#33A9FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M960 160v96c0 88.4-200.6 160-448 160S64 344.4 64 256V160C64 71.6 264.6 0 512 0s448 71.6 448 160z m-109.6 269.4c41.6-14.8 79.8-33.8 109.6-57.2V576c0 88.4-200.6 160-448 160S64 664.4 64 576V372.2c29.8 23.6 68 42.4 109.6 57.2C263.4 461.4 383 480 512 480s248.6-18.6 338.4-50.6zM64 692.2c29.8 23.6 68 42.4 109.6 57.2C263.4 781.4 383 800 512 800s248.6-18.6 338.4-50.6c41.6-14.8 79.8-33.8 109.6-57.2V864c0 88.4-200.6 160-448 160S64 952.4 64 864v-171.8z"
|
||||
fill="url(#paint0_linear_memory)"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 914 B |
36
src/pages/scenario/flowgram/assets/icon-mouse.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
export function IconMouse(props: { width?: number; height?: number }) {
|
||||
const { width, height } = props;
|
||||
return (
|
||||
<svg
|
||||
width={width || 34}
|
||||
height={height || 52}
|
||||
viewBox="0 0 34 52"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M30.9998 16.6666V35.3333C30.9998 37.5748 30.9948 38.4695 30.9 39.1895C30.2108 44.4247 26.0912 48.5443 20.856 49.2335C20.1361 49.3283 19.2413 49.3333 16.9998 49.3333C14.7584 49.3333 13.8636 49.3283 13.1437 49.2335C7.90847 48.5443 3.78888 44.4247 3.09965 39.1895C3.00487 38.4695 2.99984 37.5748 2.99984 35.3333V16.6666C2.99984 14.4252 3.00487 13.5304 3.09965 12.8105C3.78888 7.57528 7.90847 3.45569 13.1437 2.76646C13.7232 2.69017 14.4159 2.67202 15.8332 2.66785V9.86573C14.4738 10.3462 13.4998 11.6426 13.4998 13.1666V17.8332C13.4998 19.3571 14.4738 20.6536 15.8332 21.1341V23.6666C15.8332 24.3109 16.3555 24.8333 16.9998 24.8333C17.6442 24.8333 18.1665 24.3109 18.1665 23.6666V21.1341C19.5259 20.6536 20.4998 19.3572 20.4998 17.8332V13.1666C20.4998 11.6426 19.5259 10.3462 18.1665 9.86571V2.66785C19.5837 2.67202 20.2765 2.69017 20.856 2.76646C26.0912 3.45569 30.2108 7.57528 30.9 12.8105C30.9948 13.5304 30.9998 14.4252 30.9998 16.6666ZM0.666504 16.6666C0.666504 14.4993 0.666504 13.4157 0.786276 12.5059C1.61335 6.22368 6.55687 1.28016 12.8391 0.453085C13.7489 0.333313 14.8325 0.333313 16.9998 0.333313C19.1671 0.333313 20.2508 0.333313 21.1605 0.453085C27.4428 1.28016 32.3863 6.22368 33.2134 12.5059C33.3332 13.4157 33.3332 14.4994 33.3332 16.6666V35.3333C33.3332 37.5006 33.3332 38.5843 33.2134 39.494C32.3863 45.7763 27.4428 50.7198 21.1605 51.5469C20.2508 51.6666 19.1671 51.6666 16.9998 51.6666C14.8325 51.6666 13.7489 51.6666 12.8391 51.5469C6.55687 50.7198 1.61335 45.7763 0.786276 39.494C0.666504 38.5843 0.666504 37.5006 0.666504 35.3333V16.6666ZM15.8332 13.1666C15.8332 13.0011 15.8676 12.8437 15.9297 12.7011C15.9886 12.566 16.0722 12.4443 16.1749 12.3416C16.386 12.1305 16.6777 11.9999 16.9998 11.9999C17.6435 11.9999 18.1654 12.5212 18.1665 13.1646L18.1665 13.1666V17.8332L18.1665 17.8353C18.1665 17.8364 18.1665 17.8376 18.1665 17.8387C18.1661 17.9132 18.1588 17.986 18.1452 18.0565C18.0853 18.3656 17.9033 18.6312 17.6515 18.8011C17.4655 18.9266 17.2412 18.9999 16.9998 18.9999C16.3555 18.9999 15.8332 18.4776 15.8332 17.8332V13.1666Z"
|
||||
fill="currentColor"
|
||||
fillOpacity="0.8"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export const IconMouseTool = () => (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M4.5 8C4.5 4.13401 7.63401 1 11.5 1H12.5C16.366 1 19.5 4.13401 19.5 8V17C19.5 20.3137 16.8137 23 13.5 23H10.5C7.18629 23 4.5 20.3137 4.5 17V8ZM11.2517 3.00606C8.60561 3.13547 6.5 5.32184 6.5 8V17C6.5 19.2091 8.29086 21 10.5 21H13.5C15.7091 21 17.5 19.2091 17.5 17V8C17.5 5.32297 15.3962 3.13732 12.7517 3.00622V5.28013C13.2606 5.54331 13.6074 6.06549 13.6074 6.66669V8.75759C13.6074 9.35879 13.2606 9.88097 12.7517 10.1441V11.4091C12.7517 11.8233 12.4159 12.1591 12.0017 12.1591C11.5875 12.1591 11.2517 11.8233 11.2517 11.4091V10.1457C10.7411 9.88298 10.3931 9.35994 10.3931 8.75759V6.66669C10.3931 6.06433 10.7411 5.5413 11.2517 5.27862V3.00606ZM12.0017 6.14397C11.7059 6.14397 11.466 6.38381 11.466 6.67968V8.74462C11.466 9.03907 11.7036 9.27804 11.9975 9.28031L12.0002 9.28032C12.0456 9.28032 12.0896 9.27482 12.1316 9.26447C12.3401 9.21256 12.5002 9.0386 12.5318 8.82287C12.5345 8.80149 12.5359 8.7797 12.5359 8.75759V6.66669C12.5359 6.64463 12.5345 6.62288 12.5318 6.60154C12.4999 6.38354 12.3368 6.20817 12.1252 6.15826C12.0856 6.14891 12.0442 6.14397 12.0017 6.14397Z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
51
src/pages/scenario/flowgram/assets/icon-pad.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
export function IconPad(props: { width?: number; height?: number }) {
|
||||
const { width, height } = props;
|
||||
return (
|
||||
<svg
|
||||
width={width || 48}
|
||||
height={height || 38}
|
||||
viewBox="0 0 48 38"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect
|
||||
x="1.83317"
|
||||
y="1.49998"
|
||||
width="44.3333"
|
||||
height="35"
|
||||
rx="3.5"
|
||||
stroke="currentColor"
|
||||
strokeOpacity="0.8"
|
||||
strokeWidth="2.33333"
|
||||
/>
|
||||
<path
|
||||
d="M14.6665 30.6667H33.3332"
|
||||
stroke="currentColor"
|
||||
strokeOpacity="0.8"
|
||||
strokeWidth="2.33333"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export const IconPadTool = () => (
|
||||
<svg
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M20.8549 5H3.1451C3.06496 5 3 5.06496 3 5.1451V18.8549C3 18.935 3.06496 19 3.1451 19H20.8549C20.935 19 21 18.935 21 18.8549V5.1451C21 5.06496 20.935 5 20.8549 5ZM3.1451 3C1.96039 3 1 3.96039 1 5.1451V18.8549C1 20.0396 1.96039 21 3.1451 21H20.8549C22.0396 21 23 20.0396 23 18.8549V5.1451C23 3.96039 22.0396 3 20.8549 3H3.1451Z"
|
||||
></path>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M6.99991 16C6.99991 15.4477 7.44762 15 7.99991 15H15.9999C16.5522 15 16.9999 15.4477 16.9999 16C16.9999 16.5523 16.5522 17 15.9999 17H7.99991C7.44762 17 6.99991 16.5523 6.99991 16Z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
6
src/pages/scenario/flowgram/assets/icon-parallel.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg t="1724931640169" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1762"
|
||||
width="20" height="20">
|
||||
<path
|
||||
d="M1024 800a96 96 0 0 1-96 96h-192a96 96 0 0 1-96-96v-64a96 96 0 0 1 96-96h62.656c-2.56-44.416-11.84-70.72-24.576-79.36-15.36-10.304-32.896-12.928-79.552-13.632l-17.28-0.32a436.544 436.544 0 0 1-24.32-1.152l-14.72-1.472a185.792 185.792 0 0 1-75.712-24.832c-19.968-12.032-36.608-33.92-50.944-65.92-14.272 32-30.912 53.888-50.88 65.92a185.792 185.792 0 0 1-75.648 24.832l-14.72 1.472c-7.936 0.64-14.72 0.96-24.32 1.152l-17.28 0.32c-46.72 0.64-64.256 3.328-79.616 13.696-12.736 8.576-22.016 34.88-24.512 79.296H288A96 96 0 0 1 384 736v64A96 96 0 0 1 288 896h-192A96 96 0 0 1 0 800v-64A96 96 0 0 1 96 640h64.448c3.2-65.664 19.52-109.888 52.864-132.352 24.96-16.896 47.04-22.208 89.28-23.936l47.36-1.152c3.84-0.128 7.168-0.256 10.496-0.512l4.992-0.32c25.984-1.984 45.312-7.04 62.144-17.28 12.8-7.68 27.392-34.752 41.152-80.32L416 384A96 96 0 0 1 320 288v-64A96 96 0 0 1 416 128h192A96 96 0 0 1 704 224v64A96 96 0 0 1 608 384l-53.504 0.128c13.696 45.568 28.352 72.64 41.088 80.32 16.832 10.24 36.16 15.296 62.208 17.28l4.992 0.32c3.264 0.256 6.592 0.384 10.432 0.512l47.36 1.152c42.24 1.728 64.32 7.04 89.344 23.936 33.28 22.4 49.6 66.688 52.8 132.352h65.28a96 96 0 0 1 96 96z m-704 0v-64a32 32 0 0 0-32-32h-192a32 32 0 0 0-32 32v64a32 32 0 0 0 32 32h192a32 32 0 0 0 32-32z m320-512v-64a32 32 0 0 0-32-32h-192a32 32 0 0 0-32 32v64a32 32 0 0 0 32 32h192a32 32 0 0 0 32-32z m320 512v-64a32 32 0 0 0-32-32h-192a32 32 0 0 0-32 32v64a32 32 0 0 0 32 32h192a32 32 0 0 0 32-32z"
|
||||
fill="#666666" p-id="1763"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
10
src/pages/scenario/flowgram/assets/icon-robot.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="44" height="44">
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_robot" x1="1024" y1="1024" x2="8.09686" y2="4.6982" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3370FF"/>
|
||||
<stop offset="0.997908" stop-color="#33A9FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M717.12 274H762c82.842 0 150 67.158 150 150v200c0 82.842-67.158 150-150 150H262c-82.842 0-150-67.158-150-150V424c0-82.842 67.158-150 150-150h44.88l-18.268-109.602c-4.086-24.514 12.476-47.7 36.99-51.786 24.514-4.086 47.7 12.476 51.786 36.99l20 120c0.246 1.472 0.416 2.94 0.516 4.398h228.192c0.1-1.46 0.27-2.926 0.516-4.398l20-120c4.086-24.514 27.272-41.076 51.786-36.99 24.514 4.086 41.076 27.272 36.99 51.786L717.12 274zM308 484v40c0 24.852 20.148 45 45 45S398 548.852 398 524v-40c0-24.852-20.148-45-45-45S308 459.148 308 484z m318 0v40c0 24.852 20.148 45 45 45S716 548.852 716 524v-40c0-24.852-20.148-45-45-45S626 459.148 626 484zM312 912c-24.852 0-45-20.148-45-45S287.148 822 312 822h400c24.852 0 45 20.148 45 45S736.852 912 712 912H312z"
|
||||
fill="url(#paint0_linear_robot)" ></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/pages/scenario/flowgram/assets/icon-start.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
11
src/pages/scenario/flowgram/assets/icon-tool.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
width="44" height="4">
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_tool" x1="1024" y1="1024" x2="8.09686" y2="4.6982" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3370FF"/>
|
||||
<stop offset="0.997908" stop-color="#33A9FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M1024 716.8v204.8a102.4 102.4 0 0 1-102.4 102.4h-204.8v-102.4a102.4 102.4 0 0 0-102.4-102.4 102.4 102.4 0 0 0-102.4 102.4v102.4H307.2a102.4 102.4 0 0 1-102.4-102.4v-204.8H102.4a102.4 102.4 0 0 1-102.4-102.4 102.4 102.4 0 0 1 102.4-102.4h102.4V307.2c0-56.32 46.08-102.4 102.4-102.4h204.8V102.4a102.4 102.4 0 0 1 102.4-102.4 102.4 102.4 0 0 1 102.4 102.4v102.4h204.8a102.4 102.4 0 0 1 102.4 102.4v204.8h-102.4a102.4 102.4 0 0 0-102.4 102.4 102.4 102.4 0 0 0 102.4 102.4h102.4z"
|
||||
fill="url(#paint0_linear_tool)"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 939 B |
9
src/pages/scenario/flowgram/assets/icon-trycatch.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="45" viewBox="0 0 44 45" fill="none" >
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 10.3662C4 7.0525 6.68629 4.36621 10 4.36621H34C37.3137 4.36621 40 7.0525 40 10.3662V26.3662C40 29.6799 37.3137 32.3662 34 32.3662H10C6.68629 32.3662 4 29.6799 4 26.3662V10.3662ZM18.8578 15.7304L18.1723 18.4725C17.8941 19.5855 16.8941 20.3662 15.7469 20.3662H11.2415C10.413 20.3662 9.74145 19.6946 9.74145 18.8662C9.74145 18.0378 10.413 17.3662 11.2415 17.3662H15.3565L16.3224 13.5027C16.9107 11.1497 20.1682 10.9286 21.069 13.1805L24.5755 21.9468L25.8892 18.8814C26.2831 17.9622 27.187 17.3662 28.187 17.3662H32.7585C33.587 17.3662 34.2585 18.0378 34.2585 18.8662C34.2585 19.6946 33.587 20.3662 32.7585 20.3662H28.5168L26.8574 24.2381C25.98 26.2853 23.0655 26.2497 22.2383 24.1817L18.8578 15.7304ZM13 36.3662C11.8954 36.3662 11 37.2616 11 38.3662C11 39.4708 11.8954 40.3662 13 40.3662H31C32.1046 40.3662 33 39.4708 33 38.3662C33 37.2616 32.1046 36.3662 31 36.3662H13Z" fill="url(#paint0_linear_2752_183706-1)"></path>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2752_183706-1" x1="38.8417" y1="41.2869" x2="10.2834" y2="2.81176" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3370FF"></stop>
|
||||
<stop offset="0.997908" stop-color="#33A9FF"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/pages/scenario/flowgram/assets/icon-variable.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
65
src/pages/scenario/flowgram/components/agent-adder/index.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import {
|
||||
type FlowNodeEntity,
|
||||
FlowNodeRenderData,
|
||||
useClientContext,
|
||||
} from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { ToolNodeRegistry } from '../../nodes/agent/tool';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
|
||||
interface PropsType {
|
||||
node: FlowNodeEntity;
|
||||
}
|
||||
|
||||
export function AgentAdder(props: PropsType) {
|
||||
const { node } = props;
|
||||
|
||||
const nodeData = node.firstChild?.getData<FlowNodeRenderData>(FlowNodeRenderData);
|
||||
const ctx = useClientContext();
|
||||
|
||||
async function addPort() {
|
||||
ctx.operation.addNode(ToolNodeRegistry.onAdd!(ctx, node), {
|
||||
parent: node,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Tools can always be added
|
||||
* 2. LLM/Memory can only be added when there is no block
|
||||
*/
|
||||
const canAdd = node.flowNodeType === 'agentTools' || node.blocks.length === 0;
|
||||
|
||||
if (!canAdd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
color: '#fff',
|
||||
background: 'rgb(187, 191, 196)',
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
onMouseEnter={() => nodeData?.toggleMouseEnter()}
|
||||
onMouseLeave={() => nodeData?.toggleMouseLeave()}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => addPort()}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
41
src/pages/scenario/flowgram/components/agent-label/index.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';
|
||||
import { Typography } from 'antd';
|
||||
|
||||
interface PropsType {
|
||||
node: FlowNodeEntity;
|
||||
}
|
||||
|
||||
const Text = Typography.Text;
|
||||
|
||||
export function AgentLabel(props: PropsType) {
|
||||
const { node } = props;
|
||||
|
||||
let label = 'Default';
|
||||
|
||||
switch (node.flowNodeType) {
|
||||
case 'agentMemory':
|
||||
label = 'Memory';
|
||||
break;
|
||||
case 'agentLLM':
|
||||
label = 'LLM';
|
||||
break;
|
||||
case 'agentTools':
|
||||
label = 'Tools';
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
ellipsis={{ tooltip: true }}
|
||||
style={{
|
||||
maxWidth: 65,
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
padding: '2px',
|
||||
backgroundColor: 'var(--g-editor-background)',
|
||||
color: '#8F959E',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
69
src/pages/scenario/flowgram/components/base-node/index.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { useCallback } from 'react';
|
||||
|
||||
import { usePanelManager } from '@flowgram.ai/panel-manager-plugin';
|
||||
import { FlowNodeEntity, useNodeRender } from '@flowgram.ai/fixed-layout-editor';
|
||||
import { ConfigProvider } from 'antd';
|
||||
|
||||
import { NodeRenderContext } from '../../context';
|
||||
import { BaseNodeStyle, ErrorIcon } from './styles';
|
||||
import { nodeFormPanelFactory } from '../sidebar';
|
||||
|
||||
export const BaseNode = ({ node }: { node: FlowNodeEntity }) => {
|
||||
/**
|
||||
* Provides methods related to node rendering
|
||||
* 提供节点渲染相关的方法
|
||||
*/
|
||||
const nodeRender = useNodeRender();
|
||||
/**
|
||||
* It can only be used when nodeEngine is enabled
|
||||
* 只有在节点引擎开启时候才能使用表单
|
||||
*/
|
||||
const form = nodeRender.form;
|
||||
|
||||
/**
|
||||
* Used to make the Tooltip scale with the node, which can be implemented by itself depending on the UI library
|
||||
* 用于让 Tooltip 跟随节点缩放, 这个可以根据不同的 ui 库自己实现
|
||||
*/
|
||||
const getPopupContainer = useCallback(() => node.renderData.node || document.body, []);
|
||||
|
||||
const panelManager = usePanelManager();
|
||||
|
||||
return (
|
||||
<ConfigProvider getPopupContainer={getPopupContainer}>
|
||||
{form?.state.invalid && <ErrorIcon />}
|
||||
<BaseNodeStyle
|
||||
/*
|
||||
* onMouseEnter is added to a fixed layout node primarily to listen for hover highlighting of branch lines
|
||||
* onMouseEnter 加到固定布局节点主要是为了监听 分支线条的 hover 高亮
|
||||
**/
|
||||
onMouseEnter={nodeRender.onMouseEnter}
|
||||
onMouseLeave={nodeRender.onMouseLeave}
|
||||
className={nodeRender.activated ? 'activated' : ''}
|
||||
onClick={() => {
|
||||
if (nodeRender.dragging) {
|
||||
return;
|
||||
}
|
||||
panelManager.open(nodeFormPanelFactory.key, 'right', {
|
||||
props: {
|
||||
nodeId: nodeRender.node.id,
|
||||
},
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
/**
|
||||
* Lets you precisely control the style of branch nodes
|
||||
* 用于精确控制分支节点的样式
|
||||
* isBlockIcon: 整个 condition 分支的 头部节点
|
||||
* isBlockOrderIcon: 分支的第一个节点
|
||||
*/
|
||||
...(nodeRender.isBlockOrderIcon || nodeRender.isBlockIcon ? {} : {}),
|
||||
...nodeRender.node.getNodeRegistry().meta.style,
|
||||
opacity: nodeRender.dragging ? 0.3 : 1,
|
||||
outline: form?.state.invalid ? '1px solid red' : 'none',
|
||||
}}
|
||||
>
|
||||
<NodeRenderContext.Provider value={nodeRender}>{form?.render()}</NodeRenderContext.Provider>
|
||||
</BaseNodeStyle>
|
||||
</ConfigProvider>
|
||||
);
|
||||
};
|
||||
33
src/pages/scenario/flowgram/components/base-node/styles.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { InfoCircleFilled } from '@ant-design/icons';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const BaseNodeStyle = styled.div`
|
||||
align-items: flex-start;
|
||||
background-color: #fff;
|
||||
border: 1px solid rgba(6, 7, 9, 0.15);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.04), 0 4px 12px 0 rgba(0, 0, 0, 0.02);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 360px;
|
||||
cursor: default;
|
||||
&.activated {
|
||||
border: 1px solid #82a7fc;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ErrorIcon = () => (
|
||||
<InfoCircleFilled
|
||||
style={{
|
||||
position: 'absolute',
|
||||
color: 'red',
|
||||
left: -6,
|
||||
top: -6,
|
||||
zIndex: 1,
|
||||
background: 'white',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import { type FlowNodeEntity, useClientContext } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { CatchBlockNodeRegistry } from '../../nodes/catch-block';
|
||||
import { CaseNodeRegistry } from '../../nodes/case';
|
||||
import { Container } from './styles';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
|
||||
interface PropsType {
|
||||
activated?: boolean;
|
||||
node: FlowNodeEntity;
|
||||
}
|
||||
|
||||
export default function BranchAdder(props: PropsType) {
|
||||
const { activated, node } = props;
|
||||
const nodeData = node.firstChild!.renderData;
|
||||
const ctx = useClientContext();
|
||||
const { operation, playground } = ctx;
|
||||
const { isVertical } = node;
|
||||
|
||||
function addBranch() {
|
||||
const block = operation.addBlock(
|
||||
node,
|
||||
node.flowNodeType === 'switch'
|
||||
? CaseNodeRegistry.onAdd!(ctx, node)
|
||||
: CatchBlockNodeRegistry.onAdd!(ctx, node),
|
||||
{
|
||||
index: 0,
|
||||
}
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
playground.scrollToView({
|
||||
bounds: block.bounds,
|
||||
scrollToCenter: true,
|
||||
});
|
||||
}, 10);
|
||||
}
|
||||
if (playground.config.readonlyOrDisabled) return null;
|
||||
|
||||
return (
|
||||
<Container
|
||||
isVertical={isVertical}
|
||||
activated={activated}
|
||||
onMouseEnter={() => nodeData?.toggleMouseEnter()}
|
||||
onMouseLeave={() => nodeData?.toggleMouseLeave()}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
addBranch();
|
||||
}}
|
||||
aria-hidden="true"
|
||||
style={{ flexGrow: 1, textAlign: 'center' }}
|
||||
>
|
||||
<PlusOutlined />
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div<{ activated?: boolean; isVertical: boolean }>`
|
||||
width: 28px;
|
||||
height: 18px;
|
||||
background: ${(props) => (props.activated ? '#82A7FC' : 'rgb(187, 191, 196)')};
|
||||
display: flex;
|
||||
border-radius: 9px;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
transform: ${(props) => (props.isVertical ? '' : 'rotate(90deg)')};
|
||||
div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
54
src/pages/scenario/flowgram/components/drag-node/index.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import type { FlowNodeEntity, FlowNodeJSON, Xor } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { FlowNodeRegistries } from '../../nodes';
|
||||
import { Icon } from '../../form-components/form-header/styles';
|
||||
import { UIDragNodeContainer, UIDragCounts } from './styles';
|
||||
|
||||
export type PropsType = Xor<
|
||||
{
|
||||
dragStart: FlowNodeEntity;
|
||||
},
|
||||
{
|
||||
dragJSON: FlowNodeJSON;
|
||||
}
|
||||
> & {
|
||||
dragNodes: FlowNodeEntity[];
|
||||
};
|
||||
|
||||
export function DragNode(props: PropsType): JSX.Element {
|
||||
const { dragStart, dragNodes, dragJSON } = props;
|
||||
|
||||
const icon = FlowNodeRegistries.find(
|
||||
(registry) => registry.type === dragStart?.flowNodeType || dragJSON?.type
|
||||
)?.info?.icon;
|
||||
|
||||
const dragLength = (dragNodes || [])
|
||||
.map((_node) =>
|
||||
_node.allCollapsedChildren.length
|
||||
? _node.allCollapsedChildren.filter((_n) => !_n.hidden).length
|
||||
: 1
|
||||
)
|
||||
.reduce((acm, curr) => acm + curr, 0);
|
||||
|
||||
return (
|
||||
<UIDragNodeContainer>
|
||||
<Icon src={icon} />
|
||||
{dragStart?.id || dragJSON?.id}
|
||||
{dragLength > 1 && (
|
||||
<>
|
||||
<UIDragCounts>{dragLength}</UIDragCounts>
|
||||
<UIDragNodeContainer
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 5,
|
||||
right: -5,
|
||||
left: 5,
|
||||
bottom: -5,
|
||||
opacity: 0.5,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</UIDragNodeContainer>
|
||||
);
|
||||
}
|
||||
35
src/pages/scenario/flowgram/components/drag-node/styles.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
const primary = 'hsl(252 62% 54.9%)';
|
||||
const primaryOpacity09 = 'hsl(252deg 62% 55% / 9%)';
|
||||
|
||||
export const UIDragNodeContainer = styled.div`
|
||||
position: relative;
|
||||
height: 32px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 19px;
|
||||
border: 1px solid ${primary};
|
||||
padding: 0 15px;
|
||||
&:hover: {
|
||||
background-color: ${primaryOpacity09};
|
||||
color: ${primary};
|
||||
}
|
||||
`;
|
||||
|
||||
export const UIDragCounts = styled.div`
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
text-align: center;
|
||||
line-height: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
background-color: ${primary};
|
||||
`;
|
||||
3
src/pages/scenario/flowgram/components/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { DemoTools } from './tools';
|
||||
export { DragNode } from './drag-node';
|
||||
export { AgentAdder } from './agent-adder';
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
/**
|
||||
* Minimap component for workflow editor
|
||||
* Following Flowgram demo pattern from:
|
||||
* https://github.com/bytedance/flowgram.ai/blob/main/apps/demo-free-layout/src/components/tools/minimap.tsx
|
||||
*/
|
||||
import React from 'react';
|
||||
import { Minimap as FlowgramMinimap } from '@flowgram.ai/minimap-plugin';
|
||||
|
||||
export const Minimap: React.FC = () => {
|
||||
return (
|
||||
<div className="demo-tools-minimap">
|
||||
<FlowgramMinimap
|
||||
style={{
|
||||
width: '150px',
|
||||
height: '100px',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
162
src/pages/scenario/flowgram/components/node-adder/index.tsx
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { useClientContext } from '@flowgram.ai/fixed-layout-editor';
|
||||
import { type FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';
|
||||
import { Popover, message, Typography } from 'antd';
|
||||
|
||||
import { NodeList } from '../node-list';
|
||||
import { readData } from '../../shortcuts/utils';
|
||||
import { generateNodeId } from './utils';
|
||||
import { PasteIcon, Wrap } from './styles';
|
||||
import { CopyOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
|
||||
const generateNewIdForChildren = (n: FlowNodeEntity): FlowNodeEntity => {
|
||||
if (n.blocks) {
|
||||
return {
|
||||
...n,
|
||||
id: generateNodeId(n),
|
||||
blocks: n.blocks.map((b) => generateNewIdForChildren(b)),
|
||||
} as FlowNodeEntity;
|
||||
} else {
|
||||
return {
|
||||
...n,
|
||||
id: generateNodeId(n),
|
||||
} as FlowNodeEntity;
|
||||
}
|
||||
};
|
||||
|
||||
export default function Adder(props: {
|
||||
from: FlowNodeEntity;
|
||||
to?: FlowNodeEntity;
|
||||
hoverActivated: boolean;
|
||||
}) {
|
||||
const { from } = props;
|
||||
const isVertical = from.isVertical;
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { playground, operation, clipboard } = useClientContext();
|
||||
|
||||
const [pasteIconVisible, setPasteIconVisible] = useState(false);
|
||||
|
||||
const activated = useMemo(
|
||||
() => props.hoverActivated && !playground.config.readonly,
|
||||
[props.hoverActivated, playground.config.readonly]
|
||||
);
|
||||
|
||||
const add = (addProps: any) => {
|
||||
const blocks = addProps.blocks ? addProps.blocks : undefined;
|
||||
const block = operation.addFromNode(from, {
|
||||
...addProps,
|
||||
blocks,
|
||||
});
|
||||
setTimeout(() => {
|
||||
playground.scrollToView({
|
||||
bounds: block.bounds,
|
||||
scrollToCenter: true,
|
||||
});
|
||||
}, 10);
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
const handlePaste = useCallback(async (e: any) => {
|
||||
try {
|
||||
e.stopPropagation();
|
||||
const nodes = await readData(clipboard);
|
||||
|
||||
if (!nodes) {
|
||||
message.error({
|
||||
content: 'The clipboard content has been updated, please copy the node again.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
nodes.reverse().forEach((n: FlowNodeEntity) => {
|
||||
const newNodeData = generateNewIdForChildren(n);
|
||||
operation.addFromNode(from, newNodeData);
|
||||
});
|
||||
|
||||
message.success({
|
||||
content: 'Paste successfully!',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error({
|
||||
content: (
|
||||
<Typography.Text>
|
||||
Paste failed, please check if you have permission to read the clipboard,
|
||||
</Typography.Text>
|
||||
),
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
if (playground.config.readonly) return null;
|
||||
|
||||
return (
|
||||
<Popover
|
||||
visible={visible}
|
||||
onVisibleChange={setVisible}
|
||||
content={<NodeList onSelect={add} from={from} />}
|
||||
placement="right"
|
||||
trigger="click"
|
||||
align={{ offset: [30, 0] }}
|
||||
overlayStyle={{
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
<Wrap
|
||||
style={
|
||||
props.hoverActivated
|
||||
? {
|
||||
width: 15,
|
||||
height: 15,
|
||||
}
|
||||
: {}
|
||||
}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{props.hoverActivated ? (
|
||||
<PlusOutlined
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
const data = clipboard.readText();
|
||||
setPasteIconVisible(!!data);
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: '#fff',
|
||||
color: '#3370ff',
|
||||
borderRadius: 15,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{activated && pasteIconVisible && (
|
||||
<Popover placement="top" showArrow content="Paste">
|
||||
<PasteIcon
|
||||
onClick={handlePaste}
|
||||
style={
|
||||
isVertical
|
||||
? {
|
||||
right: -25,
|
||||
top: 0,
|
||||
}
|
||||
: {
|
||||
right: 0,
|
||||
top: -20,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CopyOutlined
|
||||
style={{
|
||||
backgroundColor: 'var(--semi-color-bg-0)',
|
||||
borderRadius: 15,
|
||||
}}
|
||||
/>
|
||||
</PasteIcon>
|
||||
</Popover>
|
||||
)}
|
||||
</Wrap>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
24
src/pages/scenario/flowgram/components/node-adder/styles.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const PasteIcon = styled.div`
|
||||
position: absolute;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
color: #3370ff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const Wrap = styled.div`
|
||||
position: relative;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: rgb(143, 149, 158);
|
||||
color: #fff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
import { FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
export const generateNodeId = (n: FlowNodeEntity) => `${n.type || n.flowNodeType}_${nanoid()}`;
|
||||
69
src/pages/scenario/flowgram/components/node-list.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import styled from 'styled-components';
|
||||
import {
|
||||
FlowNodeEntity,
|
||||
FlowNodeRegistry,
|
||||
useClientContext,
|
||||
} from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { FlowNodeRegistries } from '../nodes';
|
||||
|
||||
const NodeWrap = styled.div`
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 19px;
|
||||
padding: 0 15px;
|
||||
&:hover {
|
||||
background-color: hsl(252deg 62% 55% / 9%);
|
||||
color: hsl(252 62% 54.9%);
|
||||
},
|
||||
`;
|
||||
|
||||
const NodeLabel = styled.div`
|
||||
font-size: 12px;
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
function Node(props: { label: string; icon: JSX.Element; onClick: () => void; disabled: boolean }) {
|
||||
return (
|
||||
<NodeWrap
|
||||
onClick={props.disabled ? undefined : props.onClick}
|
||||
style={props.disabled ? { opacity: 0.3 } : {}}
|
||||
>
|
||||
<div style={{ fontSize: 14 }}>{props.icon}</div>
|
||||
<NodeLabel>{props.label}</NodeLabel>
|
||||
</NodeWrap>
|
||||
);
|
||||
}
|
||||
|
||||
const NodesWrap = styled.div`
|
||||
max-height: 500px;
|
||||
overflow: auto;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
export function NodeList(props: { onSelect: (meta: any) => void; from: FlowNodeEntity }) {
|
||||
const context = useClientContext();
|
||||
const handleClick = (registry: FlowNodeRegistry) => {
|
||||
const addProps = registry.onAdd(context, props.from);
|
||||
props.onSelect?.(addProps);
|
||||
};
|
||||
return (
|
||||
<NodesWrap style={{ width: 80 * 2 + 20 }}>
|
||||
{FlowNodeRegistries.filter((registry) => !registry.meta?.addDisable).map((registry) => (
|
||||
<Node
|
||||
key={registry.type}
|
||||
disabled={!(registry.canAdd?.(context, props.from) ?? true)}
|
||||
icon={<img style={{ width: 10, height: 10, borderRadius: 4 }} src={registry.info.icon} />}
|
||||
label={registry.type as string}
|
||||
onClick={() => handleClick(registry)}
|
||||
/>
|
||||
))}
|
||||
</NodesWrap>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
/**
|
||||
* NodePanel component for node type selection
|
||||
* Following Flowgram demo pattern from:
|
||||
* https://github.com/bytedance/flowgram.ai/blob/main/apps/demo-free-layout/src/components/node-panel/index.tsx
|
||||
*/
|
||||
import React from 'react';
|
||||
import { NodePanelProps } from '@flowgram.ai/free-node-panel-plugin';
|
||||
import { Card, Row, Col } from 'antd';
|
||||
import {
|
||||
ApiOutlined,
|
||||
CodeOutlined,
|
||||
BranchesOutlined,
|
||||
ClockCircleOutlined,
|
||||
SyncOutlined,
|
||||
PlayCircleOutlined,
|
||||
StopOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import intl from 'react-intl-universal';
|
||||
import { NODE_TYPES } from '../../nodes/constants';
|
||||
import './styles.less';
|
||||
|
||||
export const NodePanel: React.FC<NodePanelProps> = ({ onSelect, onClose }) => {
|
||||
const nodeTypes = [
|
||||
{
|
||||
type: NODE_TYPES.START,
|
||||
label: intl.get('scenario_start'),
|
||||
icon: <PlayCircleOutlined style={{ fontSize: 24 }} />,
|
||||
},
|
||||
{
|
||||
type: NODE_TYPES.HTTP,
|
||||
label: intl.get('scenario_http_node'),
|
||||
icon: <ApiOutlined style={{ fontSize: 24 }} />,
|
||||
},
|
||||
{
|
||||
type: NODE_TYPES.SCRIPT,
|
||||
label: intl.get('scenario_script_node'),
|
||||
icon: <CodeOutlined style={{ fontSize: 24 }} />,
|
||||
},
|
||||
{
|
||||
type: NODE_TYPES.CONDITION,
|
||||
label: intl.get('scenario_condition_node'),
|
||||
icon: <BranchesOutlined style={{ fontSize: 24 }} />,
|
||||
},
|
||||
{
|
||||
type: NODE_TYPES.DELAY,
|
||||
label: intl.get('scenario_delay_node'),
|
||||
icon: <ClockCircleOutlined style={{ fontSize: 24 }} />,
|
||||
},
|
||||
{
|
||||
type: NODE_TYPES.LOOP,
|
||||
label: intl.get('scenario_loop_node'),
|
||||
icon: <SyncOutlined style={{ fontSize: 24 }} />,
|
||||
},
|
||||
{
|
||||
type: NODE_TYPES.END,
|
||||
label: intl.get('scenario_end'),
|
||||
icon: <StopOutlined style={{ fontSize: 24 }} />,
|
||||
},
|
||||
];
|
||||
|
||||
const handleNodeClick = (type: string) => {
|
||||
onSelect?.({
|
||||
nodeType: type,
|
||||
nodeJSON: {},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="node-panel">
|
||||
<div className="node-panel-header">
|
||||
<span>{intl.get('scenario_add_node')}</span>
|
||||
</div>
|
||||
<div className="node-panel-content">
|
||||
<Row gutter={[8, 8]}>
|
||||
{nodeTypes.map((node) => (
|
||||
<Col span={12} key={node.type}>
|
||||
<Card
|
||||
hoverable
|
||||
className="node-panel-card"
|
||||
onClick={() => handleNodeClick(node.type)}
|
||||
>
|
||||
<div className="node-panel-card-content">
|
||||
{node.icon}
|
||||
<div className="node-panel-card-label">{node.label}</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
.node-panel {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
min-width: 320px;
|
||||
max-width: 400px;
|
||||
|
||||
.node-panel-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.node-panel-content {
|
||||
padding: 12px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.node-panel-card {
|
||||
.ant-card-body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.node-panel-card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-align: center;
|
||||
|
||||
.node-panel-card-label {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #1890ff;
|
||||
.node-panel-card-content {
|
||||
color: #1890ff;
|
||||
.node-panel-card-label {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode support
|
||||
[data-prefers-color='dark'] {
|
||||
.node-panel {
|
||||
background: #1f1f1f;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
|
||||
|
||||
.node-panel-header {
|
||||
border-bottom-color: #303030;
|
||||
}
|
||||
|
||||
.node-panel-card {
|
||||
background: #141414;
|
||||
border-color: #303030;
|
||||
|
||||
.node-panel-card-label {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: #177ddc;
|
||||
.node-panel-card-label {
|
||||
color: #177ddc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
import { FunctionComponent, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
useStartDragNode,
|
||||
FlowNodeRenderData,
|
||||
FlowNodeBaseType,
|
||||
FlowGroupService,
|
||||
type FlowNodeEntity,
|
||||
SelectorBoxPopoverProps,
|
||||
} from '@flowgram.ai/fixed-layout-editor';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
|
||||
import { FlowCommandId } from '../../shortcuts/constants';
|
||||
import { IconGroupOutlined } from '../../plugins/group-plugin/icons';
|
||||
import { CopyOutlined, DeleteOutlined, DragOutlined, ExpandOutlined, ShrinkOutlined } from '@ant-design/icons';
|
||||
|
||||
const BUTTON_HEIGHT = 24;
|
||||
|
||||
export const SelectorBoxPopover: FunctionComponent<SelectorBoxPopoverProps> = ({
|
||||
bounds,
|
||||
children,
|
||||
flowSelectConfig,
|
||||
commandRegistry,
|
||||
}) => {
|
||||
const selectNodes = flowSelectConfig.selectedNodes;
|
||||
|
||||
const { startDrag } = useStartDragNode();
|
||||
|
||||
const draggable = selectNodes[0]?.getData(FlowNodeRenderData)?.draggable;
|
||||
|
||||
// Does the selected component have a group node? (High-cost computation must use memo)
|
||||
const hasGroup: boolean = useMemo(() => {
|
||||
if (!selectNodes || selectNodes.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const findGroupInNodes = (nodes: FlowNodeEntity[]): boolean =>
|
||||
nodes.some((node) => {
|
||||
if (node.flowNodeType === FlowNodeBaseType.GROUP) {
|
||||
return true;
|
||||
}
|
||||
if (node.blocks && node.blocks.length) {
|
||||
return findGroupInNodes(node.blocks);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return findGroupInNodes(selectNodes);
|
||||
}, [selectNodes]);
|
||||
|
||||
const canGroup = !hasGroup && FlowGroupService.validate(selectNodes);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: bounds.right,
|
||||
top: bounds.top,
|
||||
transform: 'translate(-100%, -100%)',
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Button.Group
|
||||
size="small"
|
||||
style={{ display: 'flex', flexWrap: 'nowrap', height: BUTTON_HEIGHT }}
|
||||
>
|
||||
{draggable && (
|
||||
<Tooltip title="Drag">
|
||||
<Button
|
||||
style={{ cursor: 'grab', height: BUTTON_HEIGHT }}
|
||||
icon={<DragOutlined />}
|
||||
type="primary"
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
startDrag(e, {
|
||||
dragStartEntity: selectNodes[0],
|
||||
dragEntities: selectNodes,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip title={'Collapse'}>
|
||||
<Button
|
||||
icon={<ShrinkOutlined />}
|
||||
style={{ height: BUTTON_HEIGHT }}
|
||||
type="primary"
|
||||
onMouseDown={(e) => {
|
||||
commandRegistry.executeCommand(FlowCommandId.COLLAPSE);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={'Expand'}>
|
||||
<Button
|
||||
icon={<ExpandOutlined />}
|
||||
style={{ height: BUTTON_HEIGHT }}
|
||||
type="primary"
|
||||
onMouseDown={(e) => {
|
||||
commandRegistry.executeCommand(FlowCommandId.EXPAND);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={'Group'}>
|
||||
<Button
|
||||
icon={<IconGroupOutlined />}
|
||||
type="primary"
|
||||
style={{
|
||||
display: canGroup ? 'inherit' : 'none',
|
||||
height: BUTTON_HEIGHT,
|
||||
}}
|
||||
onClick={() => {
|
||||
commandRegistry.executeCommand(FlowCommandId.GROUP);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={'Copy'}>
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
style={{ height: BUTTON_HEIGHT }}
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
commandRegistry.executeCommand(FlowCommandId.COPY);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={'Delete'}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<DeleteOutlined />}
|
||||
style={{ height: BUTTON_HEIGHT }}
|
||||
onClick={() => {
|
||||
commandRegistry.executeCommand(FlowCommandId.DELETE);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Button.Group>
|
||||
</div>
|
||||
<div
|
||||
style={{ cursor: draggable ? 'grab' : 'auto' }}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation();
|
||||
startDrag(e, {
|
||||
dragStartEntity: selectNodes[0],
|
||||
dragEntities: selectNodes,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
1
src/pages/scenario/flowgram/components/sidebar/index.tsx
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { nodeFormPanelFactory } from './sidebar-renderer';
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { useNodeRender, FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { NodeRenderContext } from '../../context';
|
||||
|
||||
export function SidebarNodeRenderer(props: { node: FlowNodeEntity }) {
|
||||
const { node } = props;
|
||||
const nodeRender = useNodeRender(node);
|
||||
|
||||
return (
|
||||
<NodeRenderContext.Provider value={nodeRender}>
|
||||
<div
|
||||
style={{
|
||||
background: 'rgb(251, 251, 251)',
|
||||
height: '100%',
|
||||
borderRadius: 8,
|
||||
border: '1px solid rgba(82,100,154, 0.13)',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
{nodeRender.form?.render()}
|
||||
</div>
|
||||
</NodeRenderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import { useCallback, useEffect, startTransition } from 'react';
|
||||
|
||||
import { type PanelFactory, usePanelManager } from '@flowgram.ai/panel-manager-plugin';
|
||||
import {
|
||||
PlaygroundEntityContext,
|
||||
useRefresh,
|
||||
useClientContext,
|
||||
} from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { FlowNodeMeta } from '../../typings';
|
||||
import { IsSidebarContext } from '../../context';
|
||||
import { SidebarNodeRenderer } from './sidebar-node-renderer';
|
||||
|
||||
export interface NodeFormPanelProps {
|
||||
nodeId: string;
|
||||
}
|
||||
|
||||
export const SidebarRenderer: React.FC<NodeFormPanelProps> = ({ nodeId }) => {
|
||||
const panelManager = usePanelManager();
|
||||
const { selection, playground, document } = useClientContext();
|
||||
const refresh = useRefresh();
|
||||
const handleClose = useCallback(() => {
|
||||
// Sidebar delayed closing
|
||||
startTransition(() => {
|
||||
panelManager.close(nodeFormPanelFactory.key);
|
||||
});
|
||||
}, []);
|
||||
const node = nodeId ? document.getNode(nodeId) : undefined;
|
||||
/**
|
||||
* Listen readonly
|
||||
*/
|
||||
useEffect(() => {
|
||||
const disposable = playground.config.onReadonlyOrDisabledChange(() => {
|
||||
handleClose();
|
||||
refresh();
|
||||
});
|
||||
return () => disposable.dispose();
|
||||
}, [playground]);
|
||||
/**
|
||||
* Listen selection
|
||||
*/
|
||||
useEffect(() => {
|
||||
const toDispose = selection.onSelectionChanged(() => {
|
||||
/**
|
||||
* 如果没有选中任何节点,则自动关闭侧边栏
|
||||
* If no node is selected, the sidebar is automatically closed
|
||||
*/
|
||||
if (selection.selection.length === 0) {
|
||||
handleClose();
|
||||
} else if (selection.selection.length === 1 && selection.selection[0] !== node) {
|
||||
handleClose();
|
||||
}
|
||||
});
|
||||
return () => toDispose.dispose();
|
||||
}, [selection, handleClose, node]);
|
||||
/**
|
||||
* Close when node disposed
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (node) {
|
||||
const toDispose = node.onDispose(() => {
|
||||
panelManager.close(nodeFormPanelFactory.key);
|
||||
});
|
||||
return () => toDispose.dispose();
|
||||
}
|
||||
return () => {};
|
||||
}, [node]);
|
||||
|
||||
if (!node || node.getNodeMeta<FlowNodeMeta>().sidebarDisabled === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (playground.config.readonly) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IsSidebarContext.Provider value={true}>
|
||||
<PlaygroundEntityContext.Provider key={node.id} value={node}>
|
||||
<SidebarNodeRenderer node={node} />
|
||||
</PlaygroundEntityContext.Provider>
|
||||
</IsSidebarContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const nodeFormPanelFactory: PanelFactory<NodeFormPanelProps> = {
|
||||
key: 'node-form-panel',
|
||||
defaultSize: 400,
|
||||
render: (props: NodeFormPanelProps) => <SidebarRenderer {...props} />,
|
||||
};
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
/**
|
||||
* Add Node dropdown component
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Button, Dropdown, Menu } from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
ApiOutlined,
|
||||
CodeOutlined,
|
||||
BranchesOutlined,
|
||||
ClockCircleOutlined,
|
||||
SyncOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { usePlayground } from '@flowgram.ai/free-layout-editor';
|
||||
import { nanoid } from 'nanoid';
|
||||
import intl from 'react-intl-universal';
|
||||
|
||||
export const AddNodeDropdown: React.FC = () => {
|
||||
const playground = usePlayground();
|
||||
|
||||
const handleAddNode = (type: string) => {
|
||||
// Get center of viewport
|
||||
const viewport = playground.viewport.getViewport();
|
||||
const centerX = viewport.x + viewport.width / 2;
|
||||
const centerY = viewport.y + viewport.height / 2;
|
||||
|
||||
// Add node at center
|
||||
const nodeId = nanoid();
|
||||
playground.nodeService.createNode({
|
||||
id: nodeId,
|
||||
type,
|
||||
x: centerX - 140, // Offset to center the node (280px width / 2)
|
||||
y: centerY - 60, // Offset to center the node (120px height / 2)
|
||||
width: type === 'start' || type === 'end' ? 120 : 280,
|
||||
height: type === 'delay' || type === 'loop' ? 100 : 120,
|
||||
});
|
||||
};
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
items={[
|
||||
{
|
||||
key: 'http',
|
||||
label: intl.get('scenario_http_node'),
|
||||
icon: <ApiOutlined />,
|
||||
onClick: () => handleAddNode('http'),
|
||||
},
|
||||
{
|
||||
key: 'script',
|
||||
label: intl.get('scenario_script_node'),
|
||||
icon: <CodeOutlined />,
|
||||
onClick: () => handleAddNode('script'),
|
||||
},
|
||||
{
|
||||
key: 'condition',
|
||||
label: intl.get('scenario_condition_node'),
|
||||
icon: <BranchesOutlined />,
|
||||
onClick: () => handleAddNode('condition'),
|
||||
},
|
||||
{
|
||||
key: 'delay',
|
||||
label: intl.get('scenario_delay_node'),
|
||||
icon: <ClockCircleOutlined />,
|
||||
onClick: () => handleAddNode('delay'),
|
||||
},
|
||||
{
|
||||
key: 'loop',
|
||||
label: intl.get('scenario_loop_node'),
|
||||
icon: <SyncOutlined />,
|
||||
onClick: () => handleAddNode('loop'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown overlay={menu} trigger={['click']} placement="topLeft">
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
style={{
|
||||
backgroundColor: 'rgba(24, 144, 255, 0.1)',
|
||||
color: '#1890ff',
|
||||
border: '1px solid rgba(24, 144, 255, 0.3)',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
{intl.get('scenario_add_node')}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
11
src/pages/scenario/flowgram/components/tools/fit-view.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { ExpandOutlined } from '@ant-design/icons';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
|
||||
export const FitView = (props: { fitView: () => void }) => (
|
||||
<Tooltip title="FitView">
|
||||
<Button
|
||||
icon={<ExpandOutlined />}
|
||||
onClick={() => props.fitView()}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
|
@ -1,66 +1,57 @@
|
|||
/**
|
||||
* Flowgram Tools Component
|
||||
* Based on: https://github.com/bytedance/flowgram.ai/tree/main/apps/demo-free-layout/src/components/tools
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { usePlayground, usePlaygroundTools, useRefresh } from '@flowgram.ai/fixed-layout-editor';
|
||||
import { Tooltip, Button } from 'antd';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Tooltip, Divider } from 'antd';
|
||||
import {
|
||||
UndoOutlined,
|
||||
RedoOutlined,
|
||||
PlusOutlined,
|
||||
ZoomInOutlined,
|
||||
ZoomOutOutlined,
|
||||
FullscreenOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useClientContext } from '@flowgram.ai/free-layout-editor';
|
||||
import { ZoomSelect } from './zoom-select';
|
||||
import { AddNodeDropdown } from './add-node-dropdown';
|
||||
import { SwitchVertical } from './switch-vertical';
|
||||
import { ToolContainer, ToolSection } from './styles';
|
||||
import { Save } from './save';
|
||||
import { Run } from './run';
|
||||
import { Readonly } from './readonly';
|
||||
import { MinimapSwitch } from './minimap-switch';
|
||||
import { Minimap } from './minimap';
|
||||
import { Interactive } from './interactive';
|
||||
import { FitView } from './fit-view';
|
||||
import { RedoOutlined, UndoOutlined } from '@ant-design/icons';
|
||||
|
||||
export const FlowgramTools: React.FC = () => {
|
||||
const { history, playground } = useClientContext();
|
||||
const [canUndo, setCanUndo] = useState(false);
|
||||
const [canRedo, setCanRedo] = useState(false);
|
||||
export const DemoTools = () => {
|
||||
const tools = usePlaygroundTools();
|
||||
const [minimapVisible, setMinimapVisible] = useState(false);
|
||||
const playground = usePlayground();
|
||||
const refresh = useRefresh();
|
||||
|
||||
useEffect(() => {
|
||||
const disposable = history.undoRedoService.onChange(() => {
|
||||
setCanUndo(history.canUndo());
|
||||
setCanRedo(history.canRedo());
|
||||
});
|
||||
const disposable = playground.config.onReadonlyOrDisabledChange(() => refresh());
|
||||
return () => disposable.dispose();
|
||||
}, [history]);
|
||||
}, [playground]);
|
||||
|
||||
return (
|
||||
<ToolContainer className="flowgram-tools">
|
||||
<ToolContainer className="fixed-demo-tools">
|
||||
<ToolSection>
|
||||
<Interactive />
|
||||
<SwitchVertical />
|
||||
<ZoomSelect />
|
||||
<Tooltip title="Fit View">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<FullscreenOutlined />}
|
||||
onClick={() => playground.viewport.fitView()}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Divider type="vertical" style={{ height: '20px', margin: '0 4px' }} />
|
||||
<FitView fitView={tools.fitView} />
|
||||
<MinimapSwitch minimapVisible={minimapVisible} setMinimapVisible={setMinimapVisible} />
|
||||
<Minimap visible={minimapVisible} />
|
||||
<Readonly />
|
||||
<Tooltip title="Undo">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<UndoOutlined />}
|
||||
disabled={!canUndo}
|
||||
onClick={() => history.undo()}
|
||||
disabled={!tools.canUndo || playground.config.readonly}
|
||||
onClick={() => tools.undo()}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Redo">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<RedoOutlined />}
|
||||
disabled={!canRedo}
|
||||
onClick={() => history.redo()}
|
||||
disabled={!tools.canRedo || playground.config.readonly}
|
||||
onClick={() => tools.redo()}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Divider type="vertical" style={{ height: '20px', margin: '0 4px' }} />
|
||||
<AddNodeDropdown />
|
||||
<Save disabled={playground.config.readonly} />
|
||||
<Run />
|
||||
</ToolSection>
|
||||
</ToolContainer>
|
||||
);
|
||||
|
|
|
|||
90
src/pages/scenario/flowgram/components/tools/interactive.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { usePlaygroundTools, PlaygroundInteractiveType } from '@flowgram.ai/fixed-layout-editor';
|
||||
import { Tooltip, Popover } from 'antd';
|
||||
|
||||
import { MousePadSelector } from './mouse-pad-selector';
|
||||
|
||||
export const CACHE_KEY = 'workflow_prefer_interactive_type';
|
||||
export const IS_MAC_OS = /(Macintosh|MacIntel|MacPPC|Mac68K|iPad)/.test(navigator.userAgent);
|
||||
|
||||
export const getPreferInteractiveType = () => {
|
||||
const data = localStorage.getItem(CACHE_KEY) as string;
|
||||
if (data && [InteractiveType.Mouse, InteractiveType.Pad].includes(data as InteractiveType)) {
|
||||
return data;
|
||||
}
|
||||
return IS_MAC_OS ? InteractiveType.Pad : InteractiveType.Mouse;
|
||||
};
|
||||
|
||||
export const setPreferInteractiveType = (type: InteractiveType) => {
|
||||
localStorage.setItem(CACHE_KEY, type);
|
||||
};
|
||||
|
||||
export enum InteractiveType {
|
||||
Mouse = 'MOUSE',
|
||||
Pad = 'PAD',
|
||||
}
|
||||
|
||||
export const Interactive = () => {
|
||||
const tools = usePlaygroundTools();
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const [interactiveType, setInteractiveType] = useState<InteractiveType>(
|
||||
() => getPreferInteractiveType() as InteractiveType
|
||||
);
|
||||
|
||||
const [showInteractivePanel, setShowInteractivePanel] = useState(false);
|
||||
|
||||
const mousePadTooltip =
|
||||
interactiveType === InteractiveType.Mouse ? 'Mouse-Friendly' : 'Touchpad-Friendly';
|
||||
|
||||
useEffect(() => {
|
||||
// read from localStorage
|
||||
const preferInteractiveType = getPreferInteractiveType();
|
||||
tools.setInteractiveType(preferInteractiveType as PlaygroundInteractiveType);
|
||||
}, []);
|
||||
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover trigger="custom" placement="top" >
|
||||
<Tooltip
|
||||
title={mousePadTooltip}
|
||||
style={{ display: showInteractivePanel ? 'none' : 'block' }}
|
||||
>
|
||||
<div className="workflow-toolbar-interactive">
|
||||
<MousePadSelector
|
||||
value={interactiveType}
|
||||
onChange={(value) => {
|
||||
setInteractiveType(value);
|
||||
setPreferInteractiveType(value);
|
||||
tools.setInteractiveType(value);
|
||||
}}
|
||||
onPopupVisibleChange={setShowInteractivePanel}
|
||||
containerStyle={{
|
||||
border: 'none',
|
||||
height: '32px',
|
||||
width: '32px',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: '2px',
|
||||
padding: '4px',
|
||||
borderRadius: 'var(--small, 6px)',
|
||||
}}
|
||||
iconStyle={{
|
||||
margin: '0',
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
}}
|
||||
arrowStyle={{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { GifOutlined } from '@ant-design/icons';
|
||||
import { Tooltip, Button } from 'antd';
|
||||
|
||||
export const MinimapSwitch = (props: {
|
||||
minimapVisible: boolean;
|
||||
setMinimapVisible: (visible: boolean) => void;
|
||||
}) => {
|
||||
const { minimapVisible, setMinimapVisible } = props;
|
||||
|
||||
return (
|
||||
<Tooltip title="Minimap">
|
||||
<Button
|
||||
icon={
|
||||
<GifOutlined
|
||||
style={{
|
||||
color: minimapVisible ? undefined : '#060709cc',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
setMinimapVisible(Boolean(!minimapVisible));
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
30
src/pages/scenario/flowgram/components/tools/minimap.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { MinimapRender } from '@flowgram.ai/minimap-plugin';
|
||||
|
||||
import { MinimapContainer } from './styles';
|
||||
|
||||
export const Minimap = ({ visible }: { visible?: boolean }) => {
|
||||
if (!visible) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<MinimapContainer>
|
||||
<MinimapRender
|
||||
panelStyles={{}}
|
||||
containerStyles={{
|
||||
pointerEvents: 'auto',
|
||||
position: 'relative',
|
||||
top: 'unset',
|
||||
right: 'unset',
|
||||
bottom: 'unset',
|
||||
left: 'unset',
|
||||
}}
|
||||
inactiveStyle={{
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
translateX: 0,
|
||||
translateY: 0,
|
||||
}}
|
||||
/>
|
||||
</MinimapContainer>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
/* stylelint-disable no-descending-specificity */
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
.ui-mouse-pad-selector {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
box-sizing: border-box;
|
||||
width: 68px;
|
||||
height: 32px;
|
||||
padding: 8px 12px;
|
||||
|
||||
border: 1px solid rgba(29, 28, 35, 8%);
|
||||
border-radius: 8px;
|
||||
|
||||
&-icon {
|
||||
height: 20px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
height: 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&-popover {
|
||||
padding: 16px;
|
||||
|
||||
&-options {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.mouse-pad-option {
|
||||
box-sizing: border-box;
|
||||
width: 220px;
|
||||
padding-bottom: 20px;
|
||||
|
||||
text-align: center;
|
||||
|
||||
background: var(--coz-mg-card, #FFF);
|
||||
border: 1px solid var(--coz-stroke-plus, rgba(6, 7, 9, 15%));
|
||||
border-radius: var(--default, 8px);
|
||||
|
||||
&-icon {
|
||||
padding-top: 26px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
&-subTitle {
|
||||
padding: 4px 12px 0;
|
||||
}
|
||||
|
||||
&-icon-selected {
|
||||
color: rgb(19 0 221);
|
||||
}
|
||||
|
||||
&-title-selected {
|
||||
color: var(--coz-fg-hglt, #4E40E5);
|
||||
}
|
||||
|
||||
&-subTitle-selected {
|
||||
color: var(--coz-fg-hglt, #4E40E5);
|
||||
}
|
||||
|
||||
&-selected {
|
||||
cursor: pointer;
|
||||
background-color: var(--coz-mg-hglt, rgba(186, 192, 255, 20%));
|
||||
border: 1px solid var(--coz-stroke-hglt, #4E40E5);
|
||||
border-radius: var(--default, 8px);
|
||||
}
|
||||
|
||||
&:hover:not(&-selected) {
|
||||
cursor: pointer;
|
||||
|
||||
background-color: var(--coz-mg-card-hovered, #FFF);
|
||||
border: 1px solid var(--coz-stroke-plus, rgba(6, 7, 9, 15%));
|
||||
border-radius: var(--default, 8px);
|
||||
box-shadow: 0 8px 24px 0 rgba(0, 0, 0, 16%), 0 16px 48px 0 rgba(0, 0, 0, 8%);
|
||||
}
|
||||
|
||||
&:active:not(&-selected) {
|
||||
background-color: rgba(46, 46, 56, 12%);
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
padding-top: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background-color: rgba(46, 46, 56, 8%);
|
||||
border-color: rgba(77, 83, 232, 100%);
|
||||
}
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
background-color: rgba(46, 46, 56, 12%);
|
||||
border-color: rgba(77, 83, 232, 100%);
|
||||
}
|
||||
|
||||
&-active {
|
||||
border-color: rgba(77, 83, 232, 100%);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import React, { type CSSProperties, useState } from 'react';
|
||||
|
||||
import { Popover, Typography } from 'antd';
|
||||
|
||||
import { IconPad, IconPadTool } from '../../assets/icon-pad';
|
||||
import { IconMouse, IconMouseTool } from '../../assets/icon-mouse';
|
||||
|
||||
import './mouse-pad-selector.less';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
export enum InteractiveType {
|
||||
Mouse = 'MOUSE',
|
||||
Pad = 'PAD',
|
||||
}
|
||||
|
||||
export interface MousePadSelectorProps {
|
||||
value: InteractiveType;
|
||||
onChange: (value: InteractiveType) => void;
|
||||
onPopupVisibleChange?: (visible: boolean) => void;
|
||||
containerStyle?: CSSProperties;
|
||||
iconStyle?: CSSProperties;
|
||||
arrowStyle?: CSSProperties;
|
||||
}
|
||||
|
||||
const InteractiveItem: React.FC<{
|
||||
title: string;
|
||||
subTitle: string;
|
||||
icon: React.ReactNode;
|
||||
value: InteractiveType;
|
||||
selected: boolean;
|
||||
onChange: (value: InteractiveType) => void;
|
||||
}> = ({ title, subTitle, icon, onChange, value, selected }) => (
|
||||
<div
|
||||
className={`mouse-pad-option ${selected ? 'mouse-pad-option-selected' : ''}`}
|
||||
onClick={() => onChange(value)}
|
||||
>
|
||||
<div className={`mouse-pad-option-icon ${selected ? 'mouse-pad-option-icon-selected' : ''}`}>
|
||||
{icon}
|
||||
</div>
|
||||
<Title
|
||||
level={5}
|
||||
className={`mouse-pad-option-title ${selected ? 'mouse-pad-option-title-selected' : ''}`}
|
||||
>
|
||||
{title}
|
||||
</Title>
|
||||
<Paragraph
|
||||
className={`mouse-pad-option-subTitle ${
|
||||
selected ? 'mouse-pad-option-subTitle-selected' : ''
|
||||
}`}
|
||||
>
|
||||
{subTitle}
|
||||
</Paragraph>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const MousePadSelector: React.FC<
|
||||
MousePadSelectorProps & React.RefAttributes<HTMLDivElement>
|
||||
> = ({ value, onChange, onPopupVisibleChange, containerStyle, iconStyle, arrowStyle }) => {
|
||||
const isMouse = value === InteractiveType.Mouse;
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
trigger="custom"
|
||||
placement="topLeft"
|
||||
destroyTooltipOnHide
|
||||
visible={visible}
|
||||
onVisibleChange={(v) => {
|
||||
onPopupVisibleChange?.(v);
|
||||
}}
|
||||
// onClickOutside={() => {
|
||||
// setVisible(false);
|
||||
// }}
|
||||
// spacing={20}
|
||||
content={
|
||||
<div className={'ui-mouse-pad-selector-popover'}>
|
||||
<Typography.Title level={4}>{'Interaction mode'}</Typography.Title>
|
||||
<div className={'ui-mouse-pad-selector-popover-options'}>
|
||||
<InteractiveItem
|
||||
title={'Mouse-Friendly'}
|
||||
subTitle={'Drag the canvas with the left mouse button, zoom with the scroll wheel.'}
|
||||
value={InteractiveType.Mouse}
|
||||
selected={value === InteractiveType.Mouse}
|
||||
icon={<IconMouse />}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<InteractiveItem
|
||||
title={'Touchpad-Friendly'}
|
||||
subTitle={
|
||||
'Drag with two fingers moving in the same direction, zoom by pinching or spreading two fingers.'
|
||||
}
|
||||
value={InteractiveType.Pad}
|
||||
selected={value === InteractiveType.Pad}
|
||||
icon={<IconPad />}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`ui-mouse-pad-selector ${visible ? 'ui-mouse-pad-selector-active' : ''}`}
|
||||
onClick={() => {
|
||||
setVisible(!visible);
|
||||
}}
|
||||
style={containerStyle}
|
||||
>
|
||||
<div className={'ui-mouse-pad-selector-icon'} style={iconStyle}>
|
||||
{isMouse ? <IconMouseTool /> : <IconPadTool />}
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
18
src/pages/scenario/flowgram/components/tools/readonly.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { useCallback } from 'react';
|
||||
|
||||
import { usePlayground } from '@flowgram.ai/fixed-layout-editor';
|
||||
import { Button } from 'antd';
|
||||
import { UnlockOutlined, LockOutlined } from '@ant-design/icons';
|
||||
|
||||
export const Readonly = () => {
|
||||
const playground = usePlayground();
|
||||
const toggleReadonly = useCallback(() => {
|
||||
playground.config.readonly = !playground.config.readonly;
|
||||
}, [playground]);
|
||||
|
||||
return playground.config.readonly ? (
|
||||
<Button icon={<LockOutlined />} onClick={toggleReadonly} />
|
||||
) : (
|
||||
<Button icon={<UnlockOutlined />} onClick={toggleReadonly} />
|
||||
);
|
||||
};
|
||||
108
src/pages/scenario/flowgram/components/tools/run.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import {
|
||||
usePlayground,
|
||||
FlowNodeEntity,
|
||||
FixedLayoutPluginContext,
|
||||
useClientContext,
|
||||
delay,
|
||||
} from '@flowgram.ai/fixed-layout-editor';
|
||||
import { Button } from 'antd';
|
||||
|
||||
const styleElement = document.createElement('style');
|
||||
const RUNNING_COLOR = 'rgb(78, 64, 229)';
|
||||
const RUNNING_INTERVAL = 1000;
|
||||
|
||||
function getRunningNodes(targetNode?: FlowNodeEntity | undefined, addChildren?: boolean): string[] {
|
||||
const result: string[] = [];
|
||||
if (targetNode) {
|
||||
result.push(targetNode.id);
|
||||
if (addChildren) {
|
||||
result.push(...targetNode.allChildren.map((n) => n.id));
|
||||
}
|
||||
if (targetNode.parent) {
|
||||
result.push(targetNode.parent.id);
|
||||
}
|
||||
if (targetNode.pre) {
|
||||
result.push(...getRunningNodes(targetNode.pre, true));
|
||||
}
|
||||
if (targetNode.parent) {
|
||||
if (targetNode.parent.pre) {
|
||||
result.push(...getRunningNodes(targetNode.parent.pre, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function clear() {
|
||||
styleElement.innerText = '';
|
||||
}
|
||||
|
||||
function runningNode(ctx: FixedLayoutPluginContext, nodeId: string) {
|
||||
const nodes = getRunningNodes(ctx.document.getNode(nodeId), true);
|
||||
if (nodes.length === 0) {
|
||||
styleElement.innerText = '';
|
||||
} else {
|
||||
const content = nodes
|
||||
.map(
|
||||
(n) => `
|
||||
path[data-line-id$="${n}"] {
|
||||
animation: flowingDash 0.5s linear infinite;
|
||||
stroke-dasharray: 8, 5;
|
||||
stroke: ${RUNNING_COLOR} !important;
|
||||
}
|
||||
marker[data-line-id$="${n}"] path {
|
||||
fill: ${RUNNING_COLOR} !important;
|
||||
}
|
||||
[data-node-id$="${n}"] {
|
||||
border: 1px dashed ${RUNNING_COLOR} !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
[data-label-id$="${n}"] {
|
||||
color: ${RUNNING_COLOR} !important;
|
||||
}
|
||||
`
|
||||
)
|
||||
.join('\n');
|
||||
styleElement.innerText = `
|
||||
@keyframes flowingDash {
|
||||
to {
|
||||
stroke-dashoffset: -13;
|
||||
}
|
||||
}
|
||||
${content}
|
||||
`;
|
||||
}
|
||||
if (!styleElement.parentNode) {
|
||||
document.body.appendChild(styleElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the simulation and highlight the lines
|
||||
*/
|
||||
export function Run() {
|
||||
const [isRunning, setRunning] = useState(false);
|
||||
const ctx = useClientContext();
|
||||
const playground = usePlayground();
|
||||
const onRun = async () => {
|
||||
setRunning(true);
|
||||
playground.config.readonly = true;
|
||||
const nodes = ctx.document.root.blocks.slice();
|
||||
while (nodes.length > 0) {
|
||||
const currentNode = nodes.shift();
|
||||
runningNode(ctx, currentNode!.id);
|
||||
await delay(RUNNING_INTERVAL);
|
||||
}
|
||||
|
||||
playground.config.readonly = false;
|
||||
clear();
|
||||
setRunning(false);
|
||||
};
|
||||
return (
|
||||
<Button onClick={onRun} loading={isRunning}>
|
||||
Run
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
56
src/pages/scenario/flowgram/components/tools/save.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
import { useClientContext, FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';
|
||||
import { Button, Badge } from 'antd';
|
||||
|
||||
export function Save(props: { disabled: boolean }) {
|
||||
const [errorCount, setErrorCount] = useState(0);
|
||||
const clientContext = useClientContext();
|
||||
|
||||
const updateValidateData = useCallback(() => {
|
||||
const allForms = clientContext.document.getAllNodes().map((node) => node.form);
|
||||
const count = allForms.filter((form) => form?.state.invalid).length;
|
||||
setErrorCount(count);
|
||||
}, [clientContext]);
|
||||
|
||||
/**
|
||||
* Validate all node and Save
|
||||
*/
|
||||
const onSave = useCallback(async () => {
|
||||
const allForms = clientContext.document.getAllNodes().map((node) => node.form);
|
||||
await Promise.all(allForms.map(async (form) => form?.validate()));
|
||||
console.log('>>>>> save data: ', clientContext.document.toJSON());
|
||||
}, [clientContext]);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Listen single node validate
|
||||
*/
|
||||
const listenSingleNodeValidate = (node: FlowNodeEntity) => {
|
||||
const form = node.form;
|
||||
if (form) {
|
||||
const formValidateDispose = form.onValidate(() => updateValidateData());
|
||||
node.onDispose(() => formValidateDispose.dispose());
|
||||
}
|
||||
};
|
||||
clientContext.document.getAllNodes().map((node) => listenSingleNodeValidate(node));
|
||||
const dispose = clientContext.document.onNodeCreate(({ node }) =>
|
||||
listenSingleNodeValidate(node)
|
||||
);
|
||||
return () => dispose.dispose();
|
||||
}, [clientContext]);
|
||||
if (errorCount === 0) {
|
||||
return (
|
||||
<Button disabled={props.disabled} onClick={onSave}>
|
||||
Save
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge count={errorCount} >
|
||||
<Button danger disabled={props.disabled} onClick={onSave}>
|
||||
Save
|
||||
</Button>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,18 +1,14 @@
|
|||
/**
|
||||
* Styled components for Flowgram tools
|
||||
*/
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const ToolContainer = styled.div`
|
||||
position: absolute;
|
||||
bottom: 16px;
|
||||
left: 16px;
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
min-width: 360px;
|
||||
pointer-events: none;
|
||||
gap: 8px;
|
||||
|
||||
z-index: 20;
|
||||
`;
|
||||
|
||||
|
|
@ -27,28 +23,18 @@ export const ToolSection = styled.div`
|
|||
height: 40px;
|
||||
padding: 0 4px;
|
||||
pointer-events: auto;
|
||||
|
||||
.ant-btn {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SelectZoom = styled.span`
|
||||
padding: 4px 8px;
|
||||
padding: 2px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(68, 83, 130, 0.25);
|
||||
font-size: 12px;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
width: 40px;
|
||||
`;
|
||||
|
||||
export const MinimapContainer = styled.div`
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
width: 198px;
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import { CloudServerOutlined } from '@ant-design/icons';
|
||||
import { usePlaygroundTools } from '@flowgram.ai/fixed-layout-editor';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
|
||||
export const SwitchVertical = () => {
|
||||
const tools = usePlaygroundTools();
|
||||
return (
|
||||
<Tooltip title={!tools.isVertical ? 'Vertical Layout' : 'Horizontal Layout'}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => tools.changeLayout()}
|
||||
icon={
|
||||
<CloudServerOutlined
|
||||
style={{
|
||||
transform: !tools.isVertical ? '' : 'rotate(90deg)',
|
||||
transition: 'transform .3s ease',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,65 +1,31 @@
|
|||
/**
|
||||
* Zoom selector component
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
|
||||
import { usePlaygroundTools } from '@flowgram.ai/fixed-layout-editor';
|
||||
import { Divider, Dropdown, Menu } from 'antd';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Dropdown, Menu } from 'antd';
|
||||
import { usePlayground, usePlaygroundTools } from '@flowgram.ai/free-layout-editor';
|
||||
import { SelectZoom } from './styles';
|
||||
|
||||
export const ZoomSelect: React.FC = () => {
|
||||
export const ZoomSelect = () => {
|
||||
const tools = usePlaygroundTools({ maxZoom: 2, minZoom: 0.25 });
|
||||
const playground = usePlayground();
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const menu = (
|
||||
<Menu
|
||||
onClick={() => setVisible(false)}
|
||||
items={[
|
||||
{
|
||||
key: 'zoomin',
|
||||
label: 'Zoom In',
|
||||
onClick: () => tools.zoomin(),
|
||||
},
|
||||
{
|
||||
key: 'zoomout',
|
||||
label: 'Zoom Out',
|
||||
onClick: () => tools.zoomout(),
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
key: '50',
|
||||
label: 'Zoom to 50%',
|
||||
onClick: () => playground.config.updateZoom(0.5),
|
||||
},
|
||||
{
|
||||
key: '100',
|
||||
label: 'Zoom to 100%',
|
||||
onClick: () => playground.config.updateZoom(1),
|
||||
},
|
||||
{
|
||||
key: '150',
|
||||
label: 'Zoom to 150%',
|
||||
onClick: () => playground.config.updateZoom(1.5),
|
||||
},
|
||||
{
|
||||
key: '200',
|
||||
label: 'Zoom to 200%',
|
||||
onClick: () => playground.config.updateZoom(2.0),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const [dropDownVisible, openDropDown] = useState(false);
|
||||
return (
|
||||
<Dropdown
|
||||
overlay={menu}
|
||||
trigger={['click']}
|
||||
visible={visible}
|
||||
onVisibleChange={setVisible}
|
||||
placement="topLeft"
|
||||
placement="top"
|
||||
visible={dropDownVisible}
|
||||
// onClickOutSide={() => openDropDown(false)}
|
||||
dropdownRender={() => (
|
||||
<Menu>
|
||||
<Menu.Item onClick={() => tools.zoomin()}>Zoomin</Menu.Item>
|
||||
<Menu.Item onClick={() => tools.zoomout()}>Zoomout</Menu.Item>
|
||||
<Divider layout="horizontal" />
|
||||
<Menu.Item onClick={() => tools.updateZoom(0.5)}>50%</Menu.Item>
|
||||
<Menu.Item onClick={() => tools.updateZoom(1)}>100%</Menu.Item>
|
||||
<Menu.Item onClick={() => tools.updateZoom(1.5)}>150%</Menu.Item>
|
||||
<Menu.Item onClick={() => tools.updateZoom(2.0)}>200%</Menu.Item>
|
||||
</Menu>
|
||||
)}
|
||||
>
|
||||
<SelectZoom>{Math.floor(tools.zoom * 100)}%</SelectZoom>
|
||||
<SelectZoom onClick={() => openDropDown(true)}>{Math.floor(tools.zoom * 100)}%</SelectZoom>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
2
src/pages/scenario/flowgram/context/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { NodeRenderContext } from './node-render-context';
|
||||
export { IsSidebarContext } from './sidebar-context';
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import React from 'react';
|
||||
|
||||
import { type NodeRenderReturnType } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
export const NodeRenderContext = React.createContext<NodeRenderReturnType>({} as any);
|
||||
3
src/pages/scenario/flowgram/context/sidebar-context.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import React from 'react';
|
||||
|
||||
export const IsSidebarContext = React.createContext<boolean>(false);
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
.flowgram-editor-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flowgram-editor-wrapper {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.flowgram-editor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
/* Ensure minimap stays within editor bounds */
|
||||
:global {
|
||||
.flowgram-minimap {
|
||||
position: absolute !important;
|
||||
z-index: 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark theme support */
|
||||
[data-theme='dark'] {
|
||||
.flowgram-editor-container {
|
||||
background: #141414;
|
||||
}
|
||||
}
|
||||
33
src/pages/scenario/flowgram/editor.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { EditorRenderer, FixedLayoutEditorProvider, FixedLayoutPluginContext } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { FlowNodeRegistries } from './nodes';
|
||||
import { initialData } from './initial-data';
|
||||
import { useEditorProps } from './hooks/use-editor-props';
|
||||
import { DemoTools } from './components';
|
||||
|
||||
import '@flowgram.ai/fixed-layout-editor/index.css';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
export const Editor = () => {
|
||||
const ref = useRef<FixedLayoutPluginContext | null>(null);
|
||||
const editorProps = useEditorProps(initialData, FlowNodeRegistries);
|
||||
|
||||
useEffect(() => {
|
||||
const toDispose = ref.current?.document.config.onChange(debounce(() => {
|
||||
// 通过 toJSON 获取画布最新的数据
|
||||
console.log(ref.current?.document.toJSON())
|
||||
}, 1000))
|
||||
|
||||
return () => toDispose?.dispose()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="doc-feature-overview">
|
||||
<FixedLayoutEditorProvider {...editorProps} ref={ref}>
|
||||
<EditorRenderer />
|
||||
<DemoTools />
|
||||
</FixedLayoutEditorProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
34
src/pages/scenario/flowgram/form-components/feedback.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import styled from 'styled-components';
|
||||
import { FieldError, FieldState, FieldWarning } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
interface StatePanelProps {
|
||||
errors?: FieldState['errors'];
|
||||
warnings?: FieldState['warnings'];
|
||||
}
|
||||
|
||||
const Error = styled.span`
|
||||
font-size: 12px;
|
||||
color: red;
|
||||
`;
|
||||
|
||||
const Warning = styled.span`
|
||||
font-size: 12px;
|
||||
color: orange;
|
||||
`;
|
||||
|
||||
export const Feedback = ({ errors, warnings }: StatePanelProps) => {
|
||||
const renderFeedbacks = (fs: FieldError[] | FieldWarning[] | undefined) => {
|
||||
if (!fs) return null;
|
||||
return fs.map((f) => <span key={f.name}>{f.message}</span>);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Error>{renderFeedbacks(errors)}</Error>
|
||||
</div>
|
||||
<div>
|
||||
<Warning>{renderFeedbacks(warnings)}</Warning>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
|
||||
import { FlowNodeRegistry } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { useIsSidebar, useNodeRenderContext } from '../../hooks';
|
||||
import { FormTitleDescription, FormWrapper } from './styles';
|
||||
|
||||
/**
|
||||
* @param props
|
||||
* @constructor
|
||||
*/
|
||||
export function FormContent(props: { children?: React.ReactNode }) {
|
||||
const { node, expanded } = useNodeRenderContext();
|
||||
const isSidebar = useIsSidebar();
|
||||
const registry = node.getNodeRegistry<FlowNodeRegistry>();
|
||||
return (
|
||||
<FormWrapper>
|
||||
<>
|
||||
{isSidebar && <FormTitleDescription>{registry.info?.description}</FormTitleDescription>}
|
||||
{(expanded || isSidebar) && props.children}
|
||||
</>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const FormWrapper = styled.div`
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
background-color: rgb(251, 251, 251);
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding: 0 12px 12px;
|
||||
`;
|
||||
|
||||
export const FormTitleDescription = styled.div`
|
||||
color: var(--semi-color-text-2);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
padding: 0px 4px;
|
||||
word-break: break-all;
|
||||
white-space: break-spaces;
|
||||
`;
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
import { useContext, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { usePanelManager } from '@flowgram.ai/panel-manager-plugin';
|
||||
import { useClientContext } from '@flowgram.ai/fixed-layout-editor';
|
||||
import { Dropdown, Button, Menu } from 'antd';
|
||||
import { CloseOutlined, LeftOutlined } from '@ant-design/icons';
|
||||
import { MenuOutlined } from '@ant-design/icons';
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import { FlowCommandId } from '../../shortcuts/constants';
|
||||
import { useIsSidebar } from '../../hooks';
|
||||
import { NodeRenderContext } from '../../context';
|
||||
import { nodeFormPanelFactory } from '../../components/sidebar';
|
||||
import { getIcon } from './utils';
|
||||
import { TitleInput } from './title-input';
|
||||
import { Header, Operators } from './styles';
|
||||
|
||||
function DropdownContent(props: { updateTitleEdit: (editing: boolean) => void }) {
|
||||
const { updateTitleEdit } = props;
|
||||
const { node, deleteNode } = useContext(NodeRenderContext);
|
||||
const clientContext = useClientContext();
|
||||
const registry = node.getNodeRegistry<FlowNodeRegistry>();
|
||||
|
||||
const handleCopy = useCallback(
|
||||
() => {
|
||||
clientContext.playground.commandService.executeCommand(FlowCommandId.COPY, node);
|
||||
// e.stopPropagation(); // Disable clicking prevents the sidebar from opening
|
||||
},
|
||||
[clientContext, node]
|
||||
);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
() => {
|
||||
deleteNode();
|
||||
// e.stopPropagation(); // Disable clicking prevents the sidebar from opening
|
||||
},
|
||||
[clientContext, node]
|
||||
);
|
||||
|
||||
const handleEditTitle = useCallback(() => {
|
||||
updateTitleEdit(true);
|
||||
}, [updateTitleEdit]);
|
||||
|
||||
const deleteDisabled = useMemo(() => {
|
||||
if (registry.canDelete) {
|
||||
return !registry.canDelete(clientContext, node);
|
||||
}
|
||||
return registry.meta!.deleteDisable;
|
||||
}, [registry, node]);
|
||||
|
||||
return (
|
||||
<Menu>
|
||||
<Menu.Item onClick={handleEditTitle}>Edit Title</Menu.Item>
|
||||
<Menu.Item onClick={handleCopy} disabled={registry.meta!.copyDisable === true}>
|
||||
Copy
|
||||
</Menu.Item>
|
||||
<Menu.Item onClick={handleDelete} disabled={deleteDisabled}>
|
||||
Delete
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormHeader() {
|
||||
const { node, expanded, startDrag, toggleExpand, readonly } = useContext(NodeRenderContext);
|
||||
const [titleEdit, updateTitleEdit] = useState<boolean>(false);
|
||||
const panelManager = usePanelManager();
|
||||
const isSidebar = useIsSidebar();
|
||||
const handleExpand = (e: React.MouseEvent) => {
|
||||
toggleExpand();
|
||||
e.stopPropagation(); // Disable clicking prevents the sidebar from opening
|
||||
};
|
||||
const handleClose = () => {
|
||||
panelManager.close(nodeFormPanelFactory.key);
|
||||
};
|
||||
|
||||
return (
|
||||
<Header
|
||||
onMouseDown={(e) => {
|
||||
// trigger drag node
|
||||
startDrag(e);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{getIcon(node)}
|
||||
<TitleInput readonly={readonly} titleEdit={titleEdit} updateTitleEdit={updateTitleEdit} />
|
||||
{node.renderData.expandable && !isSidebar && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={expanded ? <LeftOutlined /> : <LeftOutlined rotate={90} />}
|
||||
size="small"
|
||||
onClick={handleExpand}
|
||||
/>
|
||||
)}
|
||||
{readonly ? undefined : (
|
||||
<Operators>
|
||||
<Dropdown
|
||||
trigger={['hover']}
|
||||
dropdownRender={() => <DropdownContent updateTitleEdit={updateTitleEdit} />}
|
||||
>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="small"
|
||||
icon={<MenuOutlined />}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</Dropdown>
|
||||
</Operators>
|
||||
)}
|
||||
{isSidebar && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CloseOutlined />}
|
||||
size="small"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
)}
|
||||
</Header>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const Header = styled.div`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
column-gap: 8px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
|
||||
background: linear-gradient(#f2f2ff 0%, rgb(251, 251, 251) 100%);
|
||||
overflow: hidden;
|
||||
padding: 8px;
|
||||
cursor: move;
|
||||
`;
|
||||
|
||||
export const Title = styled.div`
|
||||
font-size: 20px;
|
||||
flex: 1;
|
||||
width: 0;
|
||||
`;
|
||||
|
||||
export const Icon = styled.img`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
scale: 0.8;
|
||||
border-radius: 4px;
|
||||
`;
|
||||
|
||||
export const Operators = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
`;
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { useRef, useEffect } from 'react';
|
||||
|
||||
import { Field, FieldRenderProps } from '@flowgram.ai/fixed-layout-editor';
|
||||
import { Typography, Input } from 'antd';
|
||||
|
||||
import { Title } from './styles';
|
||||
import { Feedback } from '../feedback';
|
||||
const { Text } = Typography;
|
||||
|
||||
export function TitleInput(props: {
|
||||
readonly: boolean;
|
||||
titleEdit: boolean;
|
||||
updateTitleEdit: (setEdit: boolean) => void;
|
||||
}): JSX.Element {
|
||||
const { readonly, titleEdit, updateTitleEdit } = props;
|
||||
const ref = useRef<any>();
|
||||
const titleEditing = titleEdit && !readonly;
|
||||
useEffect(() => {
|
||||
if (titleEditing) {
|
||||
ref.current?.focus();
|
||||
}
|
||||
}, [titleEditing]);
|
||||
|
||||
return (
|
||||
<Title>
|
||||
<Field name="title">
|
||||
{({ field: { value, onChange }, fieldState }: FieldRenderProps<string>) => (
|
||||
<div style={{ height: 24 }}>
|
||||
{titleEditing ? (
|
||||
<Input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
ref={ref}
|
||||
onBlur={() => updateTitleEdit(false)}
|
||||
/>
|
||||
) : (
|
||||
<Text ellipsis={{ tooltip: true }}>{value}</Text>
|
||||
)}
|
||||
<Feedback errors={fieldState?.errors} />
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</Title>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { type FlowNodeEntity } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import { Icon } from './styles';
|
||||
|
||||
export const getIcon = (node: FlowNodeEntity) => {
|
||||
const icon = node.getNodeRegistry<FlowNodeRegistry>().info?.icon;
|
||||
if (!icon) return null;
|
||||
return <Icon src={icon} />;
|
||||
};
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { DynamicValueInput, PromptEditorWithVariables } from '@flowgram.ai/form-materials';
|
||||
import { Field } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { FormItem } from '../form-item';
|
||||
import { Feedback } from '../feedback';
|
||||
import { JsonSchema } from '../../typings';
|
||||
import { useNodeRenderContext } from '../../hooks';
|
||||
|
||||
export function FormInputs() {
|
||||
const { readonly } = useNodeRenderContext();
|
||||
|
||||
return (
|
||||
<Field<JsonSchema> name="inputs">
|
||||
{({ field: inputsField }) => {
|
||||
const required = inputsField.value?.required || [];
|
||||
const properties = inputsField.value?.properties;
|
||||
if (!properties) {
|
||||
return <></>;
|
||||
}
|
||||
const content = Object.keys(properties).map((key) => {
|
||||
const property = properties[key];
|
||||
|
||||
const formComponent = property.extra?.formComponent;
|
||||
|
||||
const vertical = ['prompt-editor'].includes(formComponent || '');
|
||||
|
||||
return (
|
||||
<Field key={key} name={`inputsValues.${key}`} defaultValue={property.default}>
|
||||
{({ field, fieldState }) => (
|
||||
<FormItem
|
||||
name={key}
|
||||
vertical={vertical}
|
||||
type={property.type as string}
|
||||
required={required.includes(key)}
|
||||
>
|
||||
{formComponent === 'prompt-editor' && (
|
||||
<PromptEditorWithVariables
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
readonly={readonly}
|
||||
hasError={Object.keys(fieldState?.errors || {}).length > 0}
|
||||
/>
|
||||
)}
|
||||
{!formComponent && (
|
||||
<DynamicValueInput
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
readonly={readonly}
|
||||
hasError={Object.keys(fieldState?.errors || {}).length > 0}
|
||||
schema={property}
|
||||
/>
|
||||
)}
|
||||
<Feedback errors={fieldState?.errors} warnings={fieldState?.warnings} />
|
||||
</FormItem>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
});
|
||||
return <>{content}</>;
|
||||
}}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
export
|
||||
.form-item-type-tag {
|
||||
color: inherit;
|
||||
padding: 0 2px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
vertical-align: middle;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { DisplaySchemaTag } from '@flowgram.ai/form-materials';
|
||||
import { Typography, Tooltip } from 'antd';
|
||||
|
||||
import './index.css';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface FormItemProps {
|
||||
children: React.ReactNode;
|
||||
name: string;
|
||||
type: string;
|
||||
required?: boolean;
|
||||
description?: string;
|
||||
labelWidth?: number;
|
||||
vertical?: boolean;
|
||||
}
|
||||
export function FormItem({
|
||||
children,
|
||||
name,
|
||||
required,
|
||||
description,
|
||||
type,
|
||||
labelWidth,
|
||||
vertical,
|
||||
}: FormItemProps): JSX.Element {
|
||||
const renderTitle = useCallback(
|
||||
(showTooltip?: boolean) => (
|
||||
<div style={{ width: '0', display: 'flex', flex: '1' }}>
|
||||
<Text style={{ width: '100%' }} ellipsis={{ tooltip: !!showTooltip }}>
|
||||
{name}
|
||||
</Text>
|
||||
{required && <span style={{ color: '#f93920', paddingLeft: '2px' }}>*</span>}
|
||||
</div>
|
||||
),
|
||||
[]
|
||||
);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
marginBottom: 6,
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
...(vertical
|
||||
? { flexDirection: 'column' }
|
||||
: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
color: 'var(--semi-color-text-0)',
|
||||
width: labelWidth || 118,
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
columnGap: 4,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<DisplaySchemaTag value={{ type }} />
|
||||
{description ? <Tooltip title={description}>{renderTitle()}</Tooltip> : renderTitle(true)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { DisplayOutputs } from '@flowgram.ai/form-materials';
|
||||
|
||||
import { useIsSidebar } from '../../hooks';
|
||||
|
||||
export function FormOutputs() {
|
||||
const isSidebar = useIsSidebar();
|
||||
if (isSidebar) {
|
||||
return null;
|
||||
}
|
||||
return <DisplayOutputs displayFromScope />;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const FormOutputsContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
border-top: 1px solid var(--semi-color-border);
|
||||
padding: 8px 0 0;
|
||||
width: 100%;
|
||||
|
||||
:global(.semi-tag .semi-tag-content) {
|
||||
font-size: 10px;
|
||||
}
|
||||
`;
|
||||
7
src/pages/scenario/flowgram/form-components/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export * from './feedback';
|
||||
export * from './form-content';
|
||||
export * from './form-outputs';
|
||||
export * from './form-inputs';
|
||||
export * from './form-header';
|
||||
export * from './form-item';
|
||||
export * from './properties-edit';
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
import React, { useContext, useState } from 'react';
|
||||
|
||||
import { Button } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
|
||||
import { JsonSchema } from '../../typings';
|
||||
import { NodeRenderContext } from '../../context';
|
||||
import { PropertyEdit } from './property-edit';
|
||||
|
||||
export interface PropertiesEditProps {
|
||||
value?: Record<string, JsonSchema>;
|
||||
onChange: (value: Record<string, JsonSchema>) => void;
|
||||
useFx?: boolean;
|
||||
}
|
||||
|
||||
export const PropertiesEdit: React.FC<PropertiesEditProps> = (props) => {
|
||||
const value = (props.value || {}) as Record<string, JsonSchema>;
|
||||
const { readonly } = useContext(NodeRenderContext);
|
||||
const [newProperty, updateNewPropertyFromCache] = useState<{ key: string; value: JsonSchema }>({
|
||||
key: '',
|
||||
value: { type: 'string' },
|
||||
});
|
||||
const [newPropertyVisible, setNewPropertyVisible] = useState<boolean>();
|
||||
const clearCache = () => {
|
||||
updateNewPropertyFromCache({ key: '', value: { type: 'string' } });
|
||||
setNewPropertyVisible(false);
|
||||
};
|
||||
|
||||
// 替换对象的key时,保持顺序
|
||||
const replaceKeyAtPosition = (
|
||||
obj: Record<string, any>,
|
||||
oldKey: string,
|
||||
newKey: string,
|
||||
newValue: any
|
||||
) => {
|
||||
const keys = Object.keys(obj);
|
||||
const index = keys.indexOf(oldKey);
|
||||
|
||||
if (index === -1) {
|
||||
// 如果 oldKey 不存在,直接添加到末尾
|
||||
return { ...obj, [newKey]: newValue };
|
||||
}
|
||||
|
||||
// 在原位置替换
|
||||
const newKeys = [...keys.slice(0, index), newKey, ...keys.slice(index + 1)];
|
||||
|
||||
return newKeys.reduce((acc, key) => {
|
||||
if (key === newKey) {
|
||||
acc[key] = newValue;
|
||||
} else {
|
||||
acc[key] = obj[key];
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
};
|
||||
|
||||
const updateProperty = (
|
||||
propertyValue: JsonSchema,
|
||||
propertyKey: string,
|
||||
newPropertyKey?: string
|
||||
) => {
|
||||
if (newPropertyKey) {
|
||||
const orderedValue = replaceKeyAtPosition(value, propertyKey, newPropertyKey, propertyValue);
|
||||
props.onChange(orderedValue);
|
||||
} else {
|
||||
const newValue = { ...value };
|
||||
newValue[propertyKey] = propertyValue;
|
||||
props.onChange(newValue);
|
||||
}
|
||||
};
|
||||
const updateNewProperty = (
|
||||
propertyValue: JsonSchema,
|
||||
propertyKey: string,
|
||||
newPropertyKey?: string
|
||||
) => {
|
||||
// const newValue = { ...value }
|
||||
if (newPropertyKey) {
|
||||
if (!(newPropertyKey in value)) {
|
||||
updateProperty(propertyValue, propertyKey, newPropertyKey);
|
||||
}
|
||||
clearCache();
|
||||
} else {
|
||||
updateNewPropertyFromCache({
|
||||
key: newPropertyKey || propertyKey,
|
||||
value: propertyValue,
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{Object.keys(props.value || {}).map((key) => {
|
||||
const property = (value[key] || {}) as JsonSchema;
|
||||
return (
|
||||
<PropertyEdit
|
||||
key={key}
|
||||
propertyKey={key}
|
||||
useFx={props.useFx}
|
||||
value={property}
|
||||
disabled={readonly}
|
||||
onChange={updateProperty}
|
||||
onDelete={() => {
|
||||
const newValue = { ...value };
|
||||
delete newValue[key];
|
||||
props.onChange(newValue);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{newPropertyVisible && (
|
||||
<PropertyEdit
|
||||
propertyKey={newProperty.key}
|
||||
value={newProperty.value}
|
||||
useFx={props.useFx}
|
||||
onChange={updateNewProperty}
|
||||
onDelete={() => {
|
||||
const key = newProperty.key;
|
||||
// after onblur
|
||||
setTimeout(() => {
|
||||
const newValue = { ...value };
|
||||
delete newValue[key];
|
||||
props.onChange(newValue);
|
||||
clearCache();
|
||||
}, 10);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!readonly && (
|
||||
<div>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setNewPropertyVisible(true)}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
import React, { useState, useLayoutEffect } from 'react';
|
||||
|
||||
import { TypeSelector, DynamicValueInput } from '@flowgram.ai/form-materials';
|
||||
import { Input, Button } from 'antd';
|
||||
|
||||
import { JsonSchema } from '../../typings';
|
||||
import { LeftColumn, Row } from './styles';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
|
||||
export interface PropertyEditProps {
|
||||
propertyKey: string;
|
||||
value: JsonSchema;
|
||||
useFx?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange: (value: JsonSchema, propertyKey: string, newPropertyKey?: string) => void;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
export const PropertyEdit: React.FC<PropertyEditProps> = (props) => {
|
||||
const { value, disabled } = props;
|
||||
const [inputKey, updateKey] = useState(props.propertyKey);
|
||||
const updateProperty = (key: keyof JsonSchema, val: any) => {
|
||||
value[key] = val;
|
||||
props.onChange(value, props.propertyKey);
|
||||
};
|
||||
|
||||
const partialUpdateProperty = (val?: Partial<JsonSchema>) => {
|
||||
props.onChange({ ...value, ...val }, props.propertyKey);
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
updateKey(props.propertyKey);
|
||||
}, [props.propertyKey]);
|
||||
return (
|
||||
<Row>
|
||||
<LeftColumn>
|
||||
<TypeSelector
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
style={{ position: 'absolute', top: 2, left: 4, zIndex: 1, padding: '0 5px', height: 20 }}
|
||||
onChange={(val) => partialUpdateProperty(val)}
|
||||
/>
|
||||
<Input
|
||||
value={inputKey}
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
onChange={(v) => updateKey(v.trim())}
|
||||
onBlur={() => {
|
||||
if (inputKey !== '') {
|
||||
props.onChange(value, props.propertyKey, inputKey);
|
||||
} else {
|
||||
updateKey(props.propertyKey);
|
||||
}
|
||||
}}
|
||||
style={{ paddingLeft: 26 }}
|
||||
/>
|
||||
</LeftColumn>
|
||||
{
|
||||
<DynamicValueInput
|
||||
value={value.default}
|
||||
onChange={(val) => updateProperty('default', val)}
|
||||
schema={value}
|
||||
style={{ flexGrow: 1 }}
|
||||
/>
|
||||
}
|
||||
{props.onDelete && !disabled && (
|
||||
<Button
|
||||
style={{ marginLeft: 5, position: 'relative', top: 2 }}
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={props.onDelete}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import styled from 'styled-components';
|
||||
|
||||
export const Row = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
margin-bottom: 6px;
|
||||
`;
|
||||
|
||||
export const LeftColumn = styled.div`
|
||||
width: 120px;
|
||||
margin-right: 5px;
|
||||
position: relative;
|
||||
`;
|
||||
3
src/pages/scenario/flowgram/hooks/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { useEditorProps } from './use-editor-props';
|
||||
export { useNodeRenderContext } from './use-node-render-context';
|
||||
export { useIsSidebar } from './use-is-sidebar';
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
/**
|
||||
* Hook for adding new workflow nodes
|
||||
* Following Flowgram demo pattern from:
|
||||
* https://github.com/bytedance/flowgram.ai/blob/main/apps/demo-free-layout/src/components/add-node/use-add-node.ts
|
||||
*/
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
useService,
|
||||
WorkflowDocument,
|
||||
usePlayground,
|
||||
PositionSchema,
|
||||
WorkflowNodeEntity,
|
||||
WorkflowSelectService,
|
||||
getAntiOverlapPosition,
|
||||
WorkflowNodeMeta,
|
||||
FlowNodeBaseType,
|
||||
} from '@flowgram.ai/free-layout-editor';
|
||||
import { NodePanelResult, WorkflowNodePanelService } from '@flowgram.ai/free-node-panel-plugin';
|
||||
|
||||
// Hook to get panel position from mouse event
|
||||
const useGetPanelPosition = () => {
|
||||
const playground = usePlayground();
|
||||
return useCallback(
|
||||
(targetBoundingRect: DOMRect): PositionSchema =>
|
||||
playground.config.getPosFromMouseEvent({
|
||||
clientX: targetBoundingRect.left + 64,
|
||||
clientY: targetBoundingRect.top - 7,
|
||||
}),
|
||||
[playground]
|
||||
);
|
||||
};
|
||||
|
||||
// Hook to handle node selection
|
||||
const useSelectNode = () => {
|
||||
const selectService = useService(WorkflowSelectService);
|
||||
return useCallback(
|
||||
(node?: WorkflowNodeEntity) => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
selectService.selectNode(node);
|
||||
},
|
||||
[selectService]
|
||||
);
|
||||
};
|
||||
|
||||
const getContainerNode = (selectService: WorkflowSelectService) => {
|
||||
const { activatedNode } = selectService;
|
||||
if (!activatedNode) {
|
||||
return;
|
||||
}
|
||||
const { isContainer } = activatedNode.getNodeMeta<WorkflowNodeMeta>();
|
||||
if (isContainer) {
|
||||
return activatedNode;
|
||||
}
|
||||
const parentNode = activatedNode.parent;
|
||||
if (!parentNode || parentNode.flowNodeType === FlowNodeBaseType.ROOT) {
|
||||
return;
|
||||
}
|
||||
return parentNode;
|
||||
};
|
||||
|
||||
// Main hook for adding new nodes
|
||||
export const useAddNode = () => {
|
||||
const workflowDocument = useService(WorkflowDocument);
|
||||
const nodePanelService = useService<WorkflowNodePanelService>(WorkflowNodePanelService);
|
||||
const selectService = useService(WorkflowSelectService);
|
||||
const playground = usePlayground();
|
||||
const getPanelPosition = useGetPanelPosition();
|
||||
const select = useSelectNode();
|
||||
|
||||
return useCallback(
|
||||
async (targetBoundingRect: DOMRect): Promise<void> => {
|
||||
const panelPosition = getPanelPosition(targetBoundingRect);
|
||||
const containerNode = getContainerNode(selectService);
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
nodePanelService.callNodePanel({
|
||||
position: panelPosition,
|
||||
enableMultiAdd: true,
|
||||
containerNode,
|
||||
panelProps: {},
|
||||
onSelect: async (panelParams?: NodePanelResult) => {
|
||||
if (!panelParams) {
|
||||
return;
|
||||
}
|
||||
const { nodeType, nodeJSON } = panelParams;
|
||||
const position = Boolean(containerNode)
|
||||
? getAntiOverlapPosition(workflowDocument, {
|
||||
x: 0,
|
||||
y: 200,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const node: WorkflowNodeEntity = workflowDocument.createWorkflowNodeByType(
|
||||
nodeType,
|
||||
position,
|
||||
containerNode?.id,
|
||||
nodeJSON
|
||||
);
|
||||
|
||||
select(node);
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
[workflowDocument, nodePanelService, selectService, playground, getPanelPosition, select]
|
||||
);
|
||||
};
|
||||
290
src/pages/scenario/flowgram/hooks/use-editor-props.ts
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
import { debounce } from 'lodash-es';
|
||||
import { createPanelManagerPlugin } from '@flowgram.ai/panel-manager-plugin';
|
||||
import { createMinimapPlugin } from '@flowgram.ai/minimap-plugin';
|
||||
import { createGroupPlugin } from '@flowgram.ai/group-plugin';
|
||||
import { defaultFixedSemiMaterials } from '@flowgram.ai/fixed-semi-materials';
|
||||
import {
|
||||
FixedLayoutProps,
|
||||
FlowDocumentJSON,
|
||||
FlowLayoutDefault,
|
||||
FlowRendererKey,
|
||||
ShortcutsRegistry,
|
||||
ConstantKeys,
|
||||
} from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { type FlowNodeRegistry } from '../typings';
|
||||
import { shortcutGetter } from '../shortcuts';
|
||||
import { CustomService } from '../services';
|
||||
import { GroupBoxHeader, GroupNode } from '../plugins/group-plugin';
|
||||
import { createClipboardPlugin } from '../plugins';
|
||||
import { nodeFormPanelFactory } from '../components/sidebar';
|
||||
import { SelectorBoxPopover } from '../components/selector-box-popover';
|
||||
import NodeAdder from '../components/node-adder';
|
||||
import BranchAdder from '../components/branch-adder';
|
||||
import { BaseNode } from '../components/base-node';
|
||||
import { AgentLabel } from '../components/agent-label';
|
||||
import { DragNode, AgentAdder } from '../components';
|
||||
|
||||
export function useEditorProps(
|
||||
initialData: FlowDocumentJSON,
|
||||
nodeRegistries: FlowNodeRegistry[]
|
||||
): FixedLayoutProps {
|
||||
return useMemo<FixedLayoutProps>(
|
||||
() => ({
|
||||
/**
|
||||
* Whether to enable the background
|
||||
*/
|
||||
background: true,
|
||||
/**
|
||||
* 画布相关配置
|
||||
* Canvas-related configurations
|
||||
*/
|
||||
playground: {
|
||||
ineractiveType: 'MOUSE',
|
||||
/**
|
||||
* Prevent Mac browser gestures from turning pages
|
||||
* 阻止 mac 浏览器手势翻页
|
||||
*/
|
||||
preventGlobalGesture: true,
|
||||
},
|
||||
/**
|
||||
* Whether it is read-only or not, the node cannot be dragged in read-only mode
|
||||
*/
|
||||
readonly: false,
|
||||
/**
|
||||
* Initial data
|
||||
* 初始化数据
|
||||
*/
|
||||
initialData,
|
||||
/**
|
||||
* Node registries
|
||||
* 节点注册
|
||||
*/
|
||||
nodeRegistries,
|
||||
/**
|
||||
* Get the default node registry, which will be merged with the 'nodeRegistries'
|
||||
* 提供默认的节点注册,这个会和 nodeRegistries 做合并
|
||||
*/
|
||||
getNodeDefaultRegistry(type) {
|
||||
return {
|
||||
type,
|
||||
meta: {
|
||||
/**
|
||||
* Default expanded
|
||||
* 默认展开所有节点
|
||||
*/
|
||||
defaultExpanded: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
/**
|
||||
* 节点数据转换, 由 ctx.document.fromJSON 调用
|
||||
* Node data transformation, called by ctx.document.fromJSON
|
||||
* @param node
|
||||
* @param json
|
||||
*/
|
||||
fromNodeJSON(node, json) {
|
||||
return json;
|
||||
},
|
||||
/**
|
||||
* 节点数据转换, 由 ctx.document.toJSON 调用
|
||||
* Node data transformation, called by ctx.document.toJSON
|
||||
* @param node
|
||||
* @param json
|
||||
*/
|
||||
toNodeJSON(node, json) {
|
||||
return json;
|
||||
},
|
||||
/**
|
||||
* Set default layout
|
||||
*/
|
||||
defaultLayout: FlowLayoutDefault.VERTICAL_FIXED_LAYOUT, // or FlowLayoutDefault.HORIZONTAL_FIXED_LAYOUT
|
||||
/**
|
||||
* Style config
|
||||
*/
|
||||
constants: {
|
||||
// [ConstantKeys.NODE_SPACING]: 24,
|
||||
// [ConstantKeys.BRANCH_SPACING]: 20,
|
||||
// [ConstantKeys.INLINE_SPACING_BOTTOM]: 24,
|
||||
// [ConstantKeys.INLINE_BLOCKS_INLINE_SPACING_BOTTOM]: 13,
|
||||
// [ConstantKeys.ROUNDED_LINE_X_RADIUS]: 8,
|
||||
// [ConstantKeys.ROUNDED_LINE_Y_RADIUS]: 10,
|
||||
// [ConstantKeys.INLINE_BLOCKS_INLINE_SPACING_TOP]: 23,
|
||||
// [ConstantKeys.INLINE_BLOCKS_PADDING_BOTTOM]: 30,
|
||||
// [ConstantKeys.COLLAPSED_SPACING]: 10,
|
||||
[ConstantKeys.BASE_COLOR]: '#B8BCC1',
|
||||
[ConstantKeys.BASE_ACTIVATED_COLOR]: '#82A7FC',
|
||||
},
|
||||
/**
|
||||
* SelectBox config
|
||||
*/
|
||||
selectBox: {
|
||||
SelectorBoxPopover,
|
||||
},
|
||||
|
||||
// Config shortcuts
|
||||
shortcuts: (registry: ShortcutsRegistry, ctx) => {
|
||||
registry.addHandlers(...shortcutGetter.map((getter) => getter(ctx)));
|
||||
},
|
||||
/**
|
||||
* Drag/Drop config
|
||||
*/
|
||||
dragdrop: {
|
||||
/**
|
||||
* Callback when drag drop
|
||||
*/
|
||||
onDrop: (ctx, dropData) => {
|
||||
// console.log(
|
||||
// '>>> onDrop: ',
|
||||
// dropData.dropNode.id,
|
||||
// dropData.dragNodes.map(n => n.id),
|
||||
// );
|
||||
},
|
||||
canDrop: (ctx, dropData) =>
|
||||
// console.log(
|
||||
// '>>> canDrop: ',
|
||||
// dropData.isBranch,
|
||||
// dropData.dropNode.id,
|
||||
// dropData.dragNodes.map(n => n.id),
|
||||
// );
|
||||
true,
|
||||
},
|
||||
/**
|
||||
* Redo/Undo enable
|
||||
*/
|
||||
history: {
|
||||
enable: true,
|
||||
enableChangeNode: true, // Listen Node engine data change
|
||||
onApply: debounce((ctx, opt) => {
|
||||
if (ctx.document.disposed) return;
|
||||
// Listen change to trigger auto save
|
||||
console.log('auto save: ', ctx.document.toJSON());
|
||||
}, 100),
|
||||
},
|
||||
/**
|
||||
* Node engine enable, you can configure formMeta in the FlowNodeRegistry
|
||||
*/
|
||||
nodeEngine: {
|
||||
enable: true,
|
||||
},
|
||||
/**
|
||||
* Variable engine enable
|
||||
*/
|
||||
variableEngine: {
|
||||
enable: true,
|
||||
},
|
||||
/**
|
||||
* Materials, components can be customized based on the key
|
||||
* @see https://github.com/bytedance/flowgram.ai/blob/main/packages/materials/fixed-semi-materials/src/components/index.tsx
|
||||
* 可以通过 key 自定义 UI 组件
|
||||
*/
|
||||
materials: {
|
||||
components: {
|
||||
...defaultFixedSemiMaterials,
|
||||
[FlowRendererKey.ADDER]: NodeAdder, // Node Add Button
|
||||
[FlowRendererKey.BRANCH_ADDER]: BranchAdder, // Branch Add Button
|
||||
[FlowRendererKey.DRAG_NODE]: DragNode, // Component in node dragging
|
||||
[FlowRendererKey.SLOT_ADDER]: AgentAdder, // Agent adder
|
||||
[FlowRendererKey.SLOT_LABEL]: AgentLabel, // Agent label
|
||||
},
|
||||
renderDefaultNode: BaseNode, // node render
|
||||
renderTexts: {
|
||||
'loop-end-text': 'Loop End',
|
||||
'loop-traverse-text': 'Loop',
|
||||
'try-start-text': 'Try Start',
|
||||
'try-end-text': 'Try End',
|
||||
'catch-text': 'Catch Error',
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Bind custom service
|
||||
*/
|
||||
onBind: ({ bind }) => {
|
||||
bind(CustomService).toSelf().inSingletonScope();
|
||||
},
|
||||
scroll: {
|
||||
/**
|
||||
* 限制滚动,防止节点都看不到
|
||||
* Limit scrolling so that none of the nodes can see it
|
||||
*/
|
||||
enableScrollLimit: true,
|
||||
},
|
||||
/**
|
||||
* Playground init
|
||||
*/
|
||||
onInit: (ctx) => {
|
||||
/**
|
||||
* Data can also be dynamically loaded via fromJSON
|
||||
* 也可以通过 fromJSON 动态加载数据
|
||||
*/
|
||||
// ctx.document.fromJSON(initialData)
|
||||
console.log('---- Playground Init ----');
|
||||
},
|
||||
/**
|
||||
* Playground render
|
||||
*/
|
||||
onAllLayersRendered: (ctx) => {
|
||||
setTimeout(() => {
|
||||
// fitView all nodes
|
||||
ctx.tools.fitView();
|
||||
}, 10);
|
||||
console.log(ctx.document.toString(true)); // Get the document tree
|
||||
},
|
||||
/**
|
||||
* Playground dispose
|
||||
*/
|
||||
onDispose: () => {
|
||||
console.log('---- Playground Dispose ----');
|
||||
},
|
||||
plugins: () => [
|
||||
/**
|
||||
* Minimap plugin
|
||||
* 缩略图插件
|
||||
*/
|
||||
createMinimapPlugin({
|
||||
disableLayer: true,
|
||||
enableDisplayAllNodes: true,
|
||||
canvasStyle: {
|
||||
canvasWidth: 182,
|
||||
canvasHeight: 102,
|
||||
canvasPadding: 50,
|
||||
canvasBackground: 'rgba(245, 245, 245, 1)',
|
||||
canvasBorderRadius: 10,
|
||||
viewportBackground: 'rgba(235, 235, 235, 1)',
|
||||
viewportBorderRadius: 4,
|
||||
viewportBorderColor: 'rgba(201, 201, 201, 1)',
|
||||
viewportBorderWidth: 1,
|
||||
viewportBorderDashLength: 2,
|
||||
nodeColor: 'rgba(255, 255, 255, 1)',
|
||||
nodeBorderRadius: 2,
|
||||
nodeBorderWidth: 0.145,
|
||||
nodeBorderColor: 'rgba(6, 7, 9, 0.10)',
|
||||
overlayColor: 'rgba(255, 255, 255, 0)',
|
||||
},
|
||||
}),
|
||||
/**
|
||||
* Group plugin
|
||||
* 分组插件
|
||||
*/
|
||||
createGroupPlugin({
|
||||
components: {
|
||||
GroupBoxHeader,
|
||||
GroupNode,
|
||||
},
|
||||
}),
|
||||
/**
|
||||
* Clipboard plugin
|
||||
* 剪切板插件
|
||||
*/
|
||||
createClipboardPlugin(),
|
||||
|
||||
createPanelManagerPlugin({
|
||||
factories: [nodeFormPanelFactory],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { useMemo } from 'react';
|
||||
import { FreeLayoutProps } from '@flowgram.ai/free-layout-editor';
|
||||
import { createFreeSnapPlugin } from '@flowgram.ai/free-snap-plugin';
|
||||
import { createFreeLinesPlugin } from '@flowgram.ai/free-lines-plugin';
|
||||
import { createFreeNodePanelPlugin } from '@flowgram.ai/free-node-panel-plugin';
|
||||
import { createPanelManagerPlugin } from '@flowgram.ai/panel-manager-plugin';
|
||||
import { createHistoryNodePlugin } from '@flowgram.ai/history-node-plugin';
|
||||
import { FlowNodeRegistry } from '../nodes/http';
|
||||
import { createToolsPlugin } from '../plugins/tools-plugin';
|
||||
import { NodePanel } from '../components/node-panel';
|
||||
|
||||
export function useEditorProps(
|
||||
initialData: any,
|
||||
nodeRegistries: FlowNodeRegistry[]
|
||||
): FreeLayoutProps {
|
||||
return useMemo<FreeLayoutProps>(
|
||||
() => ({
|
||||
background: true,
|
||||
playground: {
|
||||
preventGlobalGesture: true,
|
||||
},
|
||||
readonly: false,
|
||||
twoWayConnection: true,
|
||||
initialData,
|
||||
nodeRegistries,
|
||||
plugins: () => [
|
||||
createFreeSnapPlugin({}),
|
||||
createFreeLinesPlugin({}),
|
||||
createFreeNodePanelPlugin({
|
||||
renderer: NodePanel,
|
||||
}),
|
||||
createHistoryNodePlugin({}),
|
||||
createPanelManagerPlugin({
|
||||
factories: [],
|
||||
layerProps: {},
|
||||
}),
|
||||
createToolsPlugin(),
|
||||
],
|
||||
onChange: (data) => {
|
||||
console.log('Workflow changed:', data);
|
||||
},
|
||||
}),
|
||||
[initialData, nodeRegistries]
|
||||
);
|
||||
}
|
||||
7
src/pages/scenario/flowgram/hooks/use-is-sidebar.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { useContext } from 'react';
|
||||
|
||||
import { IsSidebarContext } from '../context';
|
||||
|
||||
export function useIsSidebar() {
|
||||
return useContext(IsSidebarContext);
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { useContext } from 'react';
|
||||
|
||||
import { NodeRenderContext } from '../context';
|
||||
|
||||
export function useNodeRenderContext() {
|
||||
return useContext(NodeRenderContext);
|
||||
}
|
||||
377
src/pages/scenario/flowgram/initial-data.ts
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
import { FlowDocumentJSON } from './typings';
|
||||
|
||||
export const initialData: FlowDocumentJSON = {
|
||||
nodes: [
|
||||
{
|
||||
id: 'start_0',
|
||||
type: 'start',
|
||||
blocks: [],
|
||||
data: {
|
||||
title: 'Start',
|
||||
outputs: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
default: 'Hello Flow.',
|
||||
},
|
||||
enable: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
array_obj: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
int: {
|
||||
type: 'number',
|
||||
},
|
||||
str: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'agent_0',
|
||||
type: 'agent',
|
||||
data: {
|
||||
title: 'Agent',
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
id: 'agentLLM_0',
|
||||
type: 'agentLLM',
|
||||
blocks: [
|
||||
{
|
||||
id: 'llm_5',
|
||||
type: 'llm',
|
||||
meta: {
|
||||
defaultExpanded: false,
|
||||
},
|
||||
data: {
|
||||
title: 'LLM',
|
||||
inputsValues: {
|
||||
modelType: {
|
||||
type: 'constant',
|
||||
content: 'gpt-3.5-turbo',
|
||||
},
|
||||
temperature: {
|
||||
type: 'constant',
|
||||
content: 0.5,
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'template',
|
||||
content: '# Role\nYou are an AI assistant.\n',
|
||||
},
|
||||
prompt: {
|
||||
type: 'template',
|
||||
content: '',
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
type: 'object',
|
||||
required: ['modelType', 'temperature', 'prompt'],
|
||||
properties: {
|
||||
modelType: {
|
||||
type: 'string',
|
||||
},
|
||||
temperature: {
|
||||
type: 'number',
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'string',
|
||||
extra: { formComponent: 'prompt-editor' },
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
extra: { formComponent: 'prompt-editor' },
|
||||
},
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
result: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'agentMemory_0',
|
||||
type: 'agentMemory',
|
||||
blocks: [
|
||||
{
|
||||
id: 'memory_0',
|
||||
type: 'memory',
|
||||
meta: {
|
||||
defaultExpanded: false,
|
||||
},
|
||||
data: {
|
||||
title: 'Memory',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'agentTools_0',
|
||||
type: 'agentTools',
|
||||
blocks: [
|
||||
{
|
||||
id: 'tool_0',
|
||||
type: 'tool',
|
||||
data: {
|
||||
title: 'Tool0',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'tool_1',
|
||||
type: 'tool',
|
||||
data: {
|
||||
title: 'Tool1',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'llm_0',
|
||||
type: 'llm',
|
||||
blocks: [],
|
||||
data: {
|
||||
title: 'LLM',
|
||||
inputsValues: {
|
||||
modelType: {
|
||||
type: 'constant',
|
||||
content: 'gpt-3.5-turbo',
|
||||
},
|
||||
temperature: {
|
||||
type: 'constant',
|
||||
content: 0.5,
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'template',
|
||||
content: '# Role\nYou are an AI assistant.\n',
|
||||
},
|
||||
prompt: {
|
||||
type: 'template',
|
||||
content: '',
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
type: 'object',
|
||||
required: ['modelType', 'temperature', 'prompt'],
|
||||
properties: {
|
||||
modelType: {
|
||||
type: 'string',
|
||||
},
|
||||
temperature: {
|
||||
type: 'number',
|
||||
},
|
||||
systemPrompt: {
|
||||
type: 'string',
|
||||
extra: { formComponent: 'prompt-editor' },
|
||||
},
|
||||
prompt: {
|
||||
type: 'string',
|
||||
extra: { formComponent: 'prompt-editor' },
|
||||
},
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
result: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'switch_0',
|
||||
type: 'switch',
|
||||
data: {
|
||||
title: 'Switch',
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
id: 'case_0',
|
||||
type: 'case',
|
||||
data: {
|
||||
title: 'Case_0',
|
||||
inputsValues: {
|
||||
condition: { type: 'constant', content: true },
|
||||
},
|
||||
inputs: {
|
||||
type: 'object',
|
||||
required: ['condition'],
|
||||
properties: {
|
||||
condition: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
id: 'case_1',
|
||||
type: 'case',
|
||||
data: {
|
||||
title: 'Case_1',
|
||||
inputsValues: {
|
||||
condition: { type: 'constant', content: true },
|
||||
},
|
||||
inputs: {
|
||||
type: 'object',
|
||||
required: ['condition'],
|
||||
properties: {
|
||||
condition: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'case_default_1',
|
||||
type: 'caseDefault',
|
||||
data: {
|
||||
title: 'Default',
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'loop_0',
|
||||
type: 'loop',
|
||||
data: {
|
||||
title: 'Loop',
|
||||
loopFor: {
|
||||
type: 'ref',
|
||||
content: ['start_0', 'array_obj'],
|
||||
},
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
id: 'if_0',
|
||||
type: 'if',
|
||||
data: {
|
||||
title: 'If',
|
||||
inputsValues: {
|
||||
condition: { type: 'constant', content: true },
|
||||
},
|
||||
inputs: {
|
||||
type: 'object',
|
||||
required: ['condition'],
|
||||
properties: {
|
||||
condition: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
id: 'if_true',
|
||||
type: 'ifBlock',
|
||||
data: {
|
||||
title: 'true',
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
id: 'if_false',
|
||||
type: 'ifBlock',
|
||||
data: {
|
||||
title: 'false',
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
id: 'break_0',
|
||||
type: 'breakLoop',
|
||||
data: {
|
||||
title: 'BreakLoop',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'tryCatch_0',
|
||||
type: 'tryCatch',
|
||||
data: {
|
||||
title: 'TryCatch',
|
||||
},
|
||||
blocks: [
|
||||
{
|
||||
id: 'tryBlock_0',
|
||||
type: 'tryBlock',
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
id: 'catchBlock_0',
|
||||
type: 'catchBlock',
|
||||
data: {
|
||||
title: 'Catch Block 1',
|
||||
inputsValues: {
|
||||
condition: { type: 'constant', content: true },
|
||||
},
|
||||
inputs: {
|
||||
type: 'object',
|
||||
required: ['condition'],
|
||||
properties: {
|
||||
condition: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
{
|
||||
id: 'catchBlock_1',
|
||||
type: 'catchBlock',
|
||||
data: {
|
||||
title: 'Catch Block 2',
|
||||
inputsValues: {
|
||||
condition: { type: 'constant', content: true },
|
||||
},
|
||||
inputs: {
|
||||
type: 'object',
|
||||
required: ['condition'],
|
||||
properties: {
|
||||
condition: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
blocks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'end_0',
|
||||
type: 'end',
|
||||
blocks: [],
|
||||
data: {
|
||||
title: 'End',
|
||||
inputsValues: {
|
||||
success: { type: 'constant', content: true, schema: { type: 'boolean' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
17
src/pages/scenario/flowgram/nodes/agent/agent-llm.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { FlowNodeBaseType } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
|
||||
export const AgentLLMNodeRegistry: FlowNodeRegistry = {
|
||||
type: 'agentLLM',
|
||||
extend: FlowNodeBaseType.SLOT_BLOCK,
|
||||
meta: {
|
||||
addDisable: true,
|
||||
sidebarDisable: true,
|
||||
draggable: false,
|
||||
},
|
||||
info: {
|
||||
icon: '',
|
||||
description: 'Agent LLM.',
|
||||
},
|
||||
};
|
||||
16
src/pages/scenario/flowgram/nodes/agent/agent-memory.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { FlowNodeBaseType } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
|
||||
export const AgentMemoryNodeRegistry: FlowNodeRegistry = {
|
||||
type: 'agentMemory',
|
||||
extend: FlowNodeBaseType.SLOT_BLOCK,
|
||||
meta: {
|
||||
addDisable: true,
|
||||
sidebarDisable: true,
|
||||
},
|
||||
info: {
|
||||
icon: '',
|
||||
description: 'Agent Memory.',
|
||||
},
|
||||
};
|
||||
33
src/pages/scenario/flowgram/nodes/agent/agent-tools.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
import { FlowNodeBaseType } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
|
||||
let index = 0;
|
||||
export const AgentToolsNodeRegistry: FlowNodeRegistry = {
|
||||
type: 'agentTools',
|
||||
extend: FlowNodeBaseType.SLOT_BLOCK,
|
||||
info: {
|
||||
icon: '',
|
||||
description: 'Agent Tools.',
|
||||
},
|
||||
meta: {
|
||||
addDisable: true,
|
||||
sidebarDisable: true,
|
||||
},
|
||||
onAdd() {
|
||||
return {
|
||||
id: `tool_${nanoid(5)}`,
|
||||
type: 'agentTool',
|
||||
data: {
|
||||
title: `Tool_${++index}`,
|
||||
outputs: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
result: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
52
src/pages/scenario/flowgram/nodes/agent/agent.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
import { FlowNodeBaseType } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { LLMNodeRegistry } from '../llm';
|
||||
import { defaultFormMeta } from '../default-form-meta';
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconRobot from '../../assets/icon-robot.svg';
|
||||
import { ToolNodeRegistry } from './tool';
|
||||
import { MemoryNodeRegistry } from './memory';
|
||||
|
||||
let index = 0;
|
||||
export const AgentNodeRegistry: FlowNodeRegistry = {
|
||||
type: 'agent',
|
||||
extend: FlowNodeBaseType.SLOT,
|
||||
info: {
|
||||
icon: iconRobot,
|
||||
description: 'AI Agent.',
|
||||
},
|
||||
formMeta: defaultFormMeta,
|
||||
onAdd(ctx, from) {
|
||||
return {
|
||||
id: `agent_${nanoid(5)}`,
|
||||
type: 'agent',
|
||||
blocks: [
|
||||
{
|
||||
id: `agentLLM_${nanoid(5)}`,
|
||||
type: 'agentLLM',
|
||||
blocks: [LLMNodeRegistry.onAdd!(ctx, from)],
|
||||
},
|
||||
{
|
||||
id: `agentMemory_${nanoid(5)}`,
|
||||
type: 'agentMemory',
|
||||
blocks: [MemoryNodeRegistry.onAdd!(ctx, from)],
|
||||
},
|
||||
{
|
||||
id: `agentTools_${nanoid(5)}`,
|
||||
type: 'agentTools',
|
||||
blocks: [ToolNodeRegistry.onAdd!(ctx, from)],
|
||||
},
|
||||
],
|
||||
data: {
|
||||
title: `Agent_${++index}`,
|
||||
outputs: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
result: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
15
src/pages/scenario/flowgram/nodes/agent/index.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { ToolNodeRegistry } from './tool';
|
||||
import { MemoryNodeRegistry } from './memory';
|
||||
import { AgentToolsNodeRegistry } from './agent-tools';
|
||||
import { AgentMemoryNodeRegistry } from './agent-memory';
|
||||
import { AgentLLMNodeRegistry } from './agent-llm';
|
||||
import { AgentNodeRegistry } from './agent';
|
||||
|
||||
export const AgentNodeRegistries = [
|
||||
AgentNodeRegistry,
|
||||
AgentMemoryNodeRegistry,
|
||||
AgentToolsNodeRegistry,
|
||||
AgentLLMNodeRegistry,
|
||||
MemoryNodeRegistry,
|
||||
ToolNodeRegistry,
|
||||
];
|
||||
31
src/pages/scenario/flowgram/nodes/agent/memory.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { defaultFormMeta } from '../default-form-meta';
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconMemory from '../../assets/icon-memory.svg';
|
||||
|
||||
let index = 0;
|
||||
export const MemoryNodeRegistry: FlowNodeRegistry = {
|
||||
type: 'memory',
|
||||
info: {
|
||||
icon: iconMemory,
|
||||
description: 'Memory.',
|
||||
},
|
||||
meta: {
|
||||
addDisable: true,
|
||||
// deleteDisable: true, // memory 不能单独删除,只能通过 agent
|
||||
copyDisable: true,
|
||||
draggable: false,
|
||||
selectable: false,
|
||||
},
|
||||
formMeta: defaultFormMeta,
|
||||
onAdd() {
|
||||
return {
|
||||
id: `memory_${nanoid(5)}`,
|
||||
type: 'memory',
|
||||
data: {
|
||||
title: `Memory_${++index}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
30
src/pages/scenario/flowgram/nodes/agent/tool.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { defaultFormMeta } from '../default-form-meta';
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconTool from '../../assets/icon-tool.svg';
|
||||
|
||||
let index = 0;
|
||||
export const ToolNodeRegistry: FlowNodeRegistry = {
|
||||
type: 'tool',
|
||||
info: {
|
||||
icon: iconTool,
|
||||
description: 'Tool.',
|
||||
},
|
||||
meta: {
|
||||
// addDisable: true,
|
||||
copyDisable: true,
|
||||
draggable: false,
|
||||
selectable: false,
|
||||
},
|
||||
formMeta: defaultFormMeta,
|
||||
onAdd() {
|
||||
return {
|
||||
id: `tool${nanoid(5)}`,
|
||||
type: 'tool',
|
||||
data: {
|
||||
title: `Tool_${++index}`,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
13
src/pages/scenario/flowgram/nodes/break-loop/form-meta.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { FormMeta } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { FormHeader } from '../../form-components';
|
||||
|
||||
export const renderForm = () => (
|
||||
<>
|
||||
<FormHeader />
|
||||
</>
|
||||
);
|
||||
|
||||
export const formMeta: FormMeta = {
|
||||
render: renderForm,
|
||||
};
|
||||
42
src/pages/scenario/flowgram/nodes/break-loop/index.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconBreak from '../../assets/icon-break.svg';
|
||||
import { formMeta } from './form-meta';
|
||||
|
||||
/**
|
||||
* Break 节点用于在 loop 中根据条件终止并跳出
|
||||
*/
|
||||
export const BreakLoopNodeRegistry: FlowNodeRegistry = {
|
||||
type: 'breakLoop',
|
||||
extend: 'end',
|
||||
info: {
|
||||
icon: iconBreak,
|
||||
description: 'Break in current Loop.',
|
||||
},
|
||||
meta: {
|
||||
style: {
|
||||
width: 240,
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Render node via formMeta
|
||||
*/
|
||||
formMeta,
|
||||
canAdd(ctx, from) {
|
||||
while (from.parent) {
|
||||
if (from.parent.flowNodeType === 'loop') return true;
|
||||
from = from.parent;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
onAdd(ctx, from) {
|
||||
return {
|
||||
id: `break_${nanoid()}`,
|
||||
type: 'breakLoop',
|
||||
data: {
|
||||
title: 'BreakLoop',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
32
src/pages/scenario/flowgram/nodes/case-default/form-meta.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { FlowNodeJSON } from '../../typings';
|
||||
import { FormHeader, FormContent, FormInputs, FormOutputs } from '../../form-components';
|
||||
|
||||
export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON['data']>) => (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent>
|
||||
<FormInputs />
|
||||
<FormOutputs />
|
||||
</FormContent>
|
||||
</>
|
||||
);
|
||||
|
||||
export const formMeta: FormMeta<FlowNodeJSON['data']> = {
|
||||
render: renderForm,
|
||||
validateTrigger: ValidateTrigger.onChange,
|
||||
validate: {
|
||||
'inputsValues.*': ({ value, context, formValues, name }) => {
|
||||
const valuePropetyKey = name.replace(/^inputsValues\./, '');
|
||||
const required = formValues.inputs?.required || [];
|
||||
if (
|
||||
required.includes(valuePropetyKey) &&
|
||||
(value === '' || value === undefined || value?.content === '')
|
||||
) {
|
||||
return `${valuePropetyKey} is required`;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
31
src/pages/scenario/flowgram/nodes/case-default/index.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconCase from '../../assets/icon-case.png';
|
||||
import { formMeta } from './form-meta';
|
||||
|
||||
export const CaseDefaultNodeRegistry: FlowNodeRegistry = {
|
||||
type: 'caseDefault',
|
||||
/**
|
||||
* 分支节点需要继承自 block
|
||||
* Branch nodes need to inherit from 'block'
|
||||
*/
|
||||
extend: 'case',
|
||||
meta: {
|
||||
copyDisable: true,
|
||||
addDisable: true,
|
||||
/**
|
||||
* caseDefault 永远在最后一个分支,所以不允许拖拽排序
|
||||
* "caseDefault" is always in the last branch, so dragging and sorting is not allowed.
|
||||
*/
|
||||
draggable: false,
|
||||
deleteDisable: true,
|
||||
style: {
|
||||
width: 240,
|
||||
},
|
||||
},
|
||||
info: {
|
||||
icon: iconCase,
|
||||
description: 'Switch default branch',
|
||||
},
|
||||
canDelete: (ctx, node) => false,
|
||||
formMeta,
|
||||
};
|
||||
32
src/pages/scenario/flowgram/nodes/case/form-meta.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { FlowNodeJSON } from '../../typings';
|
||||
import { FormHeader, FormContent, FormInputs, FormOutputs } from '../../form-components';
|
||||
|
||||
export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON['data']>) => (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent>
|
||||
<FormInputs />
|
||||
<FormOutputs />
|
||||
</FormContent>
|
||||
</>
|
||||
);
|
||||
|
||||
export const formMeta: FormMeta<FlowNodeJSON['data']> = {
|
||||
render: renderForm,
|
||||
validateTrigger: ValidateTrigger.onChange,
|
||||
validate: {
|
||||
'inputsValues.*': ({ value, context, formValues, name }) => {
|
||||
const valuePropetyKey = name.replace(/^inputsValues\./, '');
|
||||
const required = formValues.inputs?.required || [];
|
||||
if (
|
||||
required.includes(valuePropetyKey) &&
|
||||
(value === '' || value === undefined || value?.content === '')
|
||||
) {
|
||||
return `${valuePropetyKey} is required`;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
46
src/pages/scenario/flowgram/nodes/case/index.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconCase from '../../assets/icon-case.png';
|
||||
import { formMeta } from './form-meta';
|
||||
|
||||
let id = 2;
|
||||
export const CaseNodeRegistry: FlowNodeRegistry = {
|
||||
type: 'case',
|
||||
/**
|
||||
* 分支节点需要继承自 block
|
||||
* Branch nodes need to inherit from 'block'
|
||||
*/
|
||||
extend: 'block',
|
||||
meta: {
|
||||
copyDisable: true,
|
||||
addDisable: true,
|
||||
},
|
||||
info: {
|
||||
icon: iconCase,
|
||||
description: 'Execute the branch when the condition is met.',
|
||||
},
|
||||
canDelete: (ctx, node) => node.parent!.blocks.length >= 3,
|
||||
onAdd(ctx, from) {
|
||||
return {
|
||||
id: `Case_${nanoid(5)}`,
|
||||
type: 'case',
|
||||
data: {
|
||||
title: `Case_${id++}`,
|
||||
inputs: {
|
||||
type: 'object',
|
||||
required: ['condition'],
|
||||
inputsValues: {
|
||||
condition: '',
|
||||
},
|
||||
properties: {
|
||||
condition: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
formMeta,
|
||||
};
|
||||
32
src/pages/scenario/flowgram/nodes/catch-block/form-meta.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { FormRenderProps, FormMeta, ValidateTrigger } from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { FlowNodeJSON } from '../../typings';
|
||||
import { FormHeader, FormContent, FormInputs, FormOutputs } from '../../form-components';
|
||||
|
||||
export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON['data']>) => (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent>
|
||||
<FormInputs />
|
||||
<FormOutputs />
|
||||
</FormContent>
|
||||
</>
|
||||
);
|
||||
|
||||
export const formMeta: FormMeta<FlowNodeJSON['data']> = {
|
||||
render: renderForm,
|
||||
validateTrigger: ValidateTrigger.onChange,
|
||||
validate: {
|
||||
'inputsValues.*': ({ value, context, formValues, name }) => {
|
||||
const valuePropetyKey = name.replace(/^inputsValues\./, '');
|
||||
const required = formValues.inputs?.required || [];
|
||||
if (
|
||||
required.includes(valuePropetyKey) &&
|
||||
(value === '' || value === undefined || value?.content === '')
|
||||
) {
|
||||
return `${valuePropetyKey} is required`;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
42
src/pages/scenario/flowgram/nodes/catch-block/index.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
|
||||
import { FlowNodeRegistry } from '../../typings';
|
||||
import iconCase from '../../assets/icon-case.png';
|
||||
import { formMeta } from './form-meta';
|
||||
|
||||
let id = 3;
|
||||
export const CatchBlockNodeRegistry: FlowNodeRegistry = {
|
||||
type: 'catchBlock',
|
||||
meta: {
|
||||
copyDisable: true,
|
||||
addDisable: true,
|
||||
},
|
||||
info: {
|
||||
icon: iconCase,
|
||||
description: 'Execute the catch branch when the condition is met.',
|
||||
},
|
||||
canAdd: () => false,
|
||||
canDelete: (ctx, node) => node.parent!.blocks.length >= 2,
|
||||
onAdd(ctx, from) {
|
||||
return {
|
||||
id: `Catch_${nanoid(5)}`,
|
||||
type: 'catchBlock',
|
||||
data: {
|
||||
title: `Catch Block ${id++}`,
|
||||
inputs: {
|
||||
type: 'object',
|
||||
required: ['condition'],
|
||||
inputsValues: {
|
||||
condition: '',
|
||||
},
|
||||
properties: {
|
||||
condition: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
formMeta,
|
||||
};
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { nanoid } from 'nanoid';
|
||||
import { WorkflowNodeType } from '../constants';
|
||||
import intl from 'react-intl-universal';
|
||||
import { FlowNodeRegistry } from '../http';
|
||||
|
||||
let conditionIndex = 0;
|
||||
|
||||
export const ConditionNodeRegistry: FlowNodeRegistry = {
|
||||
type: WorkflowNodeType.CONDITION,
|
||||
info: {
|
||||
description: intl.get('条件判断'),
|
||||
},
|
||||
meta: {
|
||||
size: {
|
||||
width: 280,
|
||||
height: 120,
|
||||
},
|
||||
},
|
||||
onAdd() {
|
||||
return {
|
||||
id: `condition_${nanoid(5)}`,
|
||||
type: WorkflowNodeType.CONDITION,
|
||||
data: {
|
||||
title: `${intl.get('条件判断')} ${++conditionIndex}`,
|
||||
condition: '',
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
/**
|
||||
* Workflow node types
|
||||
*/
|
||||
export enum WorkflowNodeType {
|
||||
START = 'start',
|
||||
END = 'end',
|
||||
HTTP = 'http',
|
||||
SCRIPT = 'script',
|
||||
CONDITION = 'condition',
|
||||
DELAY = 'delay',
|
||||
LOOP = 'loop',
|
||||
}
|
||||
70
src/pages/scenario/flowgram/nodes/default-form-meta.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import {
|
||||
autoRenameRefEffect,
|
||||
provideJsonSchemaOutputs,
|
||||
syncVariableTitle,
|
||||
} from '@flowgram.ai/form-materials';
|
||||
import {
|
||||
FormRenderProps,
|
||||
FormMeta,
|
||||
ValidateTrigger,
|
||||
FeedbackLevel,
|
||||
} from '@flowgram.ai/fixed-layout-editor';
|
||||
|
||||
import { FlowNodeJSON } from '../typings';
|
||||
import { FormHeader, FormContent, FormInputs, FormOutputs } from '../form-components';
|
||||
|
||||
export const renderForm = ({ form }: FormRenderProps<FlowNodeJSON['data']>) => (
|
||||
<>
|
||||
<FormHeader />
|
||||
<FormContent>
|
||||
<FormInputs />
|
||||
<FormOutputs />
|
||||
</FormContent>
|
||||
</>
|
||||
);
|
||||
|
||||
export const defaultFormMeta: FormMeta<FlowNodeJSON['data']> = {
|
||||
render: renderForm,
|
||||
validateTrigger: ValidateTrigger.onChange,
|
||||
/**
|
||||
* Initialize (fromJSON) data transformation
|
||||
* 初始化(fromJSON) 数据转换
|
||||
* @param value
|
||||
* @param ctx
|
||||
*/
|
||||
formatOnInit: (value, ctx) => value,
|
||||
/**
|
||||
* Save (toJSON) data transformation
|
||||
* 保存(toJSON) 数据转换
|
||||
* @param value
|
||||
* @param ctx
|
||||
*/
|
||||
formatOnSubmit: (value, ctx) => value,
|
||||
/**
|
||||
* Supported writing as:
|
||||
* 1: validate as options: { title: () => {} , ... }
|
||||
* 2: validate as dynamic function: (values, ctx) => ({ title: () => {}, ... })
|
||||
*/
|
||||
validate: {
|
||||
title: ({ value }) => (value ? undefined : 'Title is required'),
|
||||
'inputsValues.*': ({ value, context, formValues, name }) => {
|
||||
const valuePropetyKey = name.replace(/^inputsValues\./, '');
|
||||
const required = formValues.inputs?.required || [];
|
||||
if (
|
||||
required.includes(valuePropetyKey) &&
|
||||
(value === '' || value === undefined || value?.content === '')
|
||||
) {
|
||||
return {
|
||||
message: `${valuePropetyKey} is required`,
|
||||
level: FeedbackLevel.Error, // Error || Warning
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
effect: {
|
||||
title: syncVariableTitle,
|
||||
outputs: provideJsonSchemaOutputs,
|
||||
inputsValues: autoRenameRefEffect,
|
||||
},
|
||||
};
|
||||