### What problem does this PR solve? feat: display the debugging results of each operator in a pop-up window #918 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.9.0
| @@ -36,6 +36,7 @@ | |||
| "react-pdf-highlighter": "^6.1.0", | |||
| "react-string-replace": "^1.1.1", | |||
| "react-syntax-highlighter": "^15.5.0", | |||
| "react18-json-view": "^0.2.8", | |||
| "reactflow": "^11.11.2", | |||
| "recharts": "^2.12.4", | |||
| "remark-gfm": "^4.0.0", | |||
| @@ -5011,9 +5012,9 @@ | |||
| "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" | |||
| }, | |||
| "node_modules/@types/lodash": { | |||
| "version": "4.14.202", | |||
| "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.14.202.tgz", | |||
| "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", | |||
| "version": "4.17.6", | |||
| "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.6.tgz", | |||
| "integrity": "sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==", | |||
| "dev": true | |||
| }, | |||
| "node_modules/@types/mdast": { | |||
| @@ -22274,6 +22275,14 @@ | |||
| "react-dom": ">=16.6.0" | |||
| } | |||
| }, | |||
| "node_modules/react18-json-view": { | |||
| "version": "0.2.8", | |||
| "resolved": "https://registry.npmmirror.com/react18-json-view/-/react18-json-view-0.2.8.tgz", | |||
| "integrity": "sha512-uJlcf5PEDaba6yTqfcDAcMSYECZ15SLcpP94mLFTa/+fa1kZANjERqKzS7YxxsrGP4+jDxt6sIaglR0PbQcKPw==", | |||
| "peerDependencies": { | |||
| "react": ">=16.8.0" | |||
| } | |||
| }, | |||
| "node_modules/reactcss": { | |||
| "version": "1.2.3", | |||
| "resolved": "https://registry.npmmirror.com/reactcss/-/reactcss-1.2.3.tgz", | |||
| @@ -47,6 +47,7 @@ | |||
| "react-pdf-highlighter": "^6.1.0", | |||
| "react-string-replace": "^1.1.1", | |||
| "react-syntax-highlighter": "^15.5.0", | |||
| "react18-json-view": "^0.2.8", | |||
| "reactflow": "^11.11.2", | |||
| "recharts": "^2.12.4", | |||
| "remark-gfm": "^4.0.0", | |||
| @@ -14,65 +14,67 @@ import OperatorIcon from '../../operator-icon'; | |||
| import CategorizeHandle from './categorize-handle'; | |||
| import NodeDropdown from './dropdown'; | |||
| import styles from './index.less'; | |||
| import NodePopover from './popover'; | |||
| export function CategorizeNode({ id, data, selected }: NodeProps<NodeData>) { | |||
| const categoryData = get(data, 'form.category_description') ?? {}; | |||
| const style = operatorMap[data.label as Operator]; | |||
| const { t } = useTranslate('flow'); | |||
| return ( | |||
| <section | |||
| className={classNames(styles.ragNode, { | |||
| [styles.selectedNode]: selected, | |||
| })} | |||
| style={{ | |||
| backgroundColor: style.backgroundColor, | |||
| color: style.color, | |||
| }} | |||
| > | |||
| <Handle | |||
| type="target" | |||
| position={Position.Left} | |||
| isConnectable | |||
| className={styles.handle} | |||
| id={'a'} | |||
| ></Handle> | |||
| <Handle | |||
| type="target" | |||
| position={Position.Top} | |||
| isConnectable | |||
| className={styles.handle} | |||
| id={'b'} | |||
| ></Handle> | |||
| <Handle | |||
| type="target" | |||
| position={Position.Bottom} | |||
| isConnectable | |||
| className={styles.handle} | |||
| id={'c'} | |||
| ></Handle> | |||
| {Object.keys(categoryData).map((x, idx) => { | |||
| console.info(categoryData, id, data); | |||
| return ( | |||
| <CategorizeHandle | |||
| top={CategorizeAnchorPointPositions[idx].top} | |||
| right={CategorizeAnchorPointPositions[idx].right} | |||
| key={idx} | |||
| text={x} | |||
| idx={idx} | |||
| ></CategorizeHandle> | |||
| ); | |||
| })} | |||
| <Flex vertical align="center" justify="center" gap={6}> | |||
| <OperatorIcon | |||
| name={data.label as Operator} | |||
| fontSize={24} | |||
| ></OperatorIcon> | |||
| <span className={styles.type}>{t(lowerFirst(data.label))}</span> | |||
| <NodeDropdown id={id}></NodeDropdown> | |||
| </Flex> | |||
| <section className={styles.bottomBox}> | |||
| <div className={styles.nodeName}>{data.name}</div> | |||
| <NodePopover nodeId={id}> | |||
| <section | |||
| className={classNames(styles.ragNode, { | |||
| [styles.selectedNode]: selected, | |||
| })} | |||
| style={{ | |||
| backgroundColor: style.backgroundColor, | |||
| color: style.color, | |||
| }} | |||
| > | |||
| <Handle | |||
| type="target" | |||
| position={Position.Left} | |||
| isConnectable | |||
| className={styles.handle} | |||
| id={'a'} | |||
| ></Handle> | |||
| <Handle | |||
| type="target" | |||
| position={Position.Top} | |||
| isConnectable | |||
| className={styles.handle} | |||
| id={'b'} | |||
| ></Handle> | |||
| <Handle | |||
| type="target" | |||
| position={Position.Bottom} | |||
| isConnectable | |||
| className={styles.handle} | |||
| id={'c'} | |||
| ></Handle> | |||
| {Object.keys(categoryData).map((x, idx) => { | |||
| return ( | |||
| <CategorizeHandle | |||
| top={CategorizeAnchorPointPositions[idx].top} | |||
| right={CategorizeAnchorPointPositions[idx].right} | |||
| key={idx} | |||
| text={x} | |||
| idx={idx} | |||
| ></CategorizeHandle> | |||
| ); | |||
| })} | |||
| <Flex vertical align="center" justify="center" gap={6}> | |||
| <OperatorIcon | |||
| name={data.label as Operator} | |||
| fontSize={24} | |||
| ></OperatorIcon> | |||
| <span className={styles.type}>{t(lowerFirst(data.label))}</span> | |||
| <NodeDropdown id={id}></NodeDropdown> | |||
| </Flex> | |||
| <section className={styles.bottomBox}> | |||
| <div className={styles.nodeName}>{data.name}</div> | |||
| </section> | |||
| </section> | |||
| </section> | |||
| </NodePopover> | |||
| ); | |||
| } | |||
| @@ -9,6 +9,7 @@ import { NodeData } from '../../interface'; | |||
| import OperatorIcon from '../../operator-icon'; | |||
| import NodeDropdown from './dropdown'; | |||
| import styles from './index.less'; | |||
| import NodePopover from './popover'; | |||
| export function RagNode({ | |||
| id, | |||
| @@ -18,53 +19,54 @@ export function RagNode({ | |||
| }: NodeProps<NodeData>) { | |||
| const style = operatorMap[data.label as Operator]; | |||
| const { t } = useTranslate('flow'); | |||
| return ( | |||
| <section | |||
| className={classNames(styles.ragNode, { | |||
| [styles.selectedNode]: selected, | |||
| })} | |||
| style={pick(style, ['backgroundColor', 'width', 'height', 'color'])} | |||
| > | |||
| <Handle | |||
| id="c" | |||
| type="source" | |||
| position={Position.Left} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| ></Handle> | |||
| <Handle type="source" position={Position.Top} id="d" isConnectable /> | |||
| <Handle | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| id="b" | |||
| ></Handle> | |||
| <Handle type="source" position={Position.Bottom} id="a" isConnectable /> | |||
| <Flex | |||
| vertical | |||
| align="center" | |||
| justify={'center'} | |||
| gap={data.label === Operator.RewriteQuestion ? 0 : 6} | |||
| <NodePopover nodeId={id}> | |||
| <section | |||
| className={classNames(styles.ragNode, { | |||
| [styles.selectedNode]: selected, | |||
| })} | |||
| style={pick(style, ['backgroundColor', 'width', 'height', 'color'])} | |||
| > | |||
| <OperatorIcon | |||
| name={data.label as Operator} | |||
| fontSize={style['iconFontSize'] ?? 24} | |||
| ></OperatorIcon> | |||
| <span | |||
| className={styles.type} | |||
| style={{ fontSize: style.fontSize ?? 14 }} | |||
| <Handle | |||
| id="c" | |||
| type="source" | |||
| position={Position.Left} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| ></Handle> | |||
| <Handle type="source" position={Position.Top} id="d" isConnectable /> | |||
| <Handle | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| id="b" | |||
| ></Handle> | |||
| <Handle type="source" position={Position.Bottom} id="a" isConnectable /> | |||
| <Flex | |||
| vertical | |||
| align="center" | |||
| justify={'center'} | |||
| gap={data.label === Operator.RewriteQuestion ? 0 : 6} | |||
| > | |||
| {data.label === Operator.RewriteQuestion | |||
| ? t(lowerFirst('Rewrite')) | |||
| : t(lowerFirst(data.label))} | |||
| </span> | |||
| <NodeDropdown id={id}></NodeDropdown> | |||
| </Flex> | |||
| <OperatorIcon | |||
| name={data.label as Operator} | |||
| fontSize={style['iconFontSize'] ?? 24} | |||
| ></OperatorIcon> | |||
| <span | |||
| className={styles.type} | |||
| style={{ fontSize: style.fontSize ?? 14 }} | |||
| > | |||
| {t(lowerFirst(data.label))} | |||
| </span> | |||
| <NodeDropdown id={id}></NodeDropdown> | |||
| </Flex> | |||
| <section className={styles.bottomBox}> | |||
| <div className={styles.nodeName}>{data.name}</div> | |||
| <section className={styles.bottomBox}> | |||
| <div className={styles.nodeName}>{data.name}</div> | |||
| </section> | |||
| </section> | |||
| </section> | |||
| </NodePopover> | |||
| ); | |||
| } | |||
| @@ -0,0 +1,47 @@ | |||
| import { useFetchFlow } from '@/hooks/flow-hooks'; | |||
| import { Popover } from 'antd'; | |||
| import get from 'lodash/get'; | |||
| import React, { useMemo } from 'react'; | |||
| import JsonView from 'react18-json-view'; | |||
| import 'react18-json-view/src/style.css'; | |||
| import { Operator } from '../../constant'; | |||
| import { useReplaceIdWithText } from '../../hooks'; | |||
| interface IProps extends React.PropsWithChildren { | |||
| nodeId: string; | |||
| } | |||
| const NodePopover = ({ children, nodeId }: IProps) => { | |||
| const { data } = useFetchFlow(); | |||
| const component = useMemo(() => { | |||
| return get(data, ['dsl', 'components', nodeId], {}); | |||
| }, [nodeId, data]); | |||
| const output = get(component, ['obj', 'params', 'output'], {}); | |||
| const componentName = get(component, ['obj', 'component_name'], ''); | |||
| const replacedOutput = useReplaceIdWithText(output); | |||
| const content = | |||
| componentName !== Operator.Answer ? ( | |||
| <div | |||
| onClick={(e) => { | |||
| e.preventDefault(); | |||
| e.stopPropagation(); | |||
| }} | |||
| > | |||
| <JsonView | |||
| src={replacedOutput} | |||
| displaySize={30} | |||
| style={{ maxWidth: 300, maxHeight: 500 }} | |||
| /> | |||
| </div> | |||
| ) : undefined; | |||
| return ( | |||
| <Popover content={content} placement="right" destroyTooltipOnHide> | |||
| {children} | |||
| </Popover> | |||
| ); | |||
| }; | |||
| export default NodePopover; | |||
| @@ -11,56 +11,59 @@ import NodeDropdown from './dropdown'; | |||
| import CategorizeHandle from './categorize-handle'; | |||
| import styles from './index.less'; | |||
| import NodePopover from './popover'; | |||
| export function RelevantNode({ id, data, selected }: NodeProps<NodeData>) { | |||
| const style = operatorMap[data.label as Operator]; | |||
| const { t } = useTranslate('flow'); | |||
| return ( | |||
| <section | |||
| className={classNames(styles.ragNode, { | |||
| [styles.selectedNode]: selected, | |||
| })} | |||
| style={pick(style, ['backgroundColor', 'width', 'height', 'color'])} | |||
| > | |||
| <Handle | |||
| type="target" | |||
| position={Position.Left} | |||
| isConnectable | |||
| className={styles.handle} | |||
| id={'a'} | |||
| ></Handle> | |||
| <Handle | |||
| type="target" | |||
| position={Position.Top} | |||
| isConnectable | |||
| className={styles.handle} | |||
| id={'b'} | |||
| ></Handle> | |||
| <Handle | |||
| type="target" | |||
| position={Position.Bottom} | |||
| isConnectable | |||
| className={styles.handle} | |||
| id={'c'} | |||
| ></Handle> | |||
| <CategorizeHandle top={20} right={6} text={'yes'}></CategorizeHandle> | |||
| <CategorizeHandle top={80} right={6} text={'no'}></CategorizeHandle> | |||
| <Flex vertical align="center" justify="center"> | |||
| <OperatorIcon | |||
| name={data.label as Operator} | |||
| fontSize={style.iconFontSize} | |||
| ></OperatorIcon> | |||
| <span | |||
| className={styles.type} | |||
| style={{ fontSize: style.fontSize ?? 14 }} | |||
| > | |||
| {t(lowerFirst(data.label))} | |||
| </span> | |||
| <NodeDropdown id={id}></NodeDropdown> | |||
| </Flex> | |||
| <section className={styles.bottomBox}> | |||
| <div className={styles.nodeName}>{data.name}</div> | |||
| <NodePopover nodeId={id}> | |||
| <section | |||
| className={classNames(styles.ragNode, { | |||
| [styles.selectedNode]: selected, | |||
| })} | |||
| style={pick(style, ['backgroundColor', 'width', 'height', 'color'])} | |||
| > | |||
| <Handle | |||
| type="target" | |||
| position={Position.Left} | |||
| isConnectable | |||
| className={styles.handle} | |||
| id={'a'} | |||
| ></Handle> | |||
| <Handle | |||
| type="target" | |||
| position={Position.Top} | |||
| isConnectable | |||
| className={styles.handle} | |||
| id={'b'} | |||
| ></Handle> | |||
| <Handle | |||
| type="target" | |||
| position={Position.Bottom} | |||
| isConnectable | |||
| className={styles.handle} | |||
| id={'c'} | |||
| ></Handle> | |||
| <CategorizeHandle top={20} right={6} text={'yes'}></CategorizeHandle> | |||
| <CategorizeHandle top={80} right={6} text={'no'}></CategorizeHandle> | |||
| <Flex vertical align="center" justify="center"> | |||
| <OperatorIcon | |||
| name={data.label as Operator} | |||
| fontSize={style.iconFontSize} | |||
| ></OperatorIcon> | |||
| <span | |||
| className={styles.type} | |||
| style={{ fontSize: style.fontSize ?? 14 }} | |||
| > | |||
| {t(lowerFirst(data.label))} | |||
| </span> | |||
| <NodeDropdown id={id}></NodeDropdown> | |||
| </Flex> | |||
| <section className={styles.bottomBox}> | |||
| <div className={styles.nodeName}>{data.name}</div> | |||
| </section> | |||
| </section> | |||
| </section> | |||
| </NodePopover> | |||
| ); | |||
| } | |||
| @@ -38,7 +38,11 @@ import { | |||
| initialRewriteQuestionValues, | |||
| } from './constant'; | |||
| import useGraphStore, { RFState } from './store'; | |||
| import { buildDslComponentsByGraph, receiveMessageError } from './utils'; | |||
| import { | |||
| buildDslComponentsByGraph, | |||
| receiveMessageError, | |||
| replaceIdWithText, | |||
| } from './utils'; | |||
| const selector = (state: RFState) => ({ | |||
| nodes: state.nodes, | |||
| @@ -376,3 +380,13 @@ export const useSaveGraphBeforeOpeningDebugDrawer = (show: () => void) => { | |||
| return handleRun; | |||
| }; | |||
| export const useReplaceIdWithText = (output: unknown) => { | |||
| const getNode = useGraphStore((state) => state.getNode); | |||
| const getNameById = (id?: string) => { | |||
| return getNode(id)?.data.name; | |||
| }; | |||
| return replaceIdWithText(output, getNameById); | |||
| }; | |||
| @@ -4,6 +4,7 @@ import dagre from 'dagre'; | |||
| import { humanId } from 'human-id'; | |||
| import { curry } from 'lodash'; | |||
| import pipe from 'lodash/fp/pipe'; | |||
| import isObject from 'lodash/isObject'; | |||
| import { Edge, Node, Position } from 'reactflow'; | |||
| import { v4 as uuidv4 } from 'uuid'; | |||
| import { NodeMap, Operator } from './constant'; | |||
| @@ -184,3 +185,26 @@ export const buildDslComponentsByGraph = ( | |||
| export const receiveMessageError = (res: any) => | |||
| res && (res?.response.status !== 200 || res?.data?.retcode !== 0); | |||
| // Replace the id in the object with text | |||
| export const replaceIdWithText = ( | |||
| obj: Record<string, unknown> | unknown[] | unknown, | |||
| getNameById: (id?: string) => string | undefined, | |||
| ) => { | |||
| if (isObject(obj)) { | |||
| const ret: Record<string, unknown> | unknown[] = Array.isArray(obj) | |||
| ? [] | |||
| : {}; | |||
| Object.keys(obj).forEach((key) => { | |||
| const val = (obj as Record<string, unknown>)[key]; | |||
| const text = typeof val === 'string' ? getNameById(val) : undefined; | |||
| (ret as Record<string, unknown>)[key] = text | |||
| ? text | |||
| : replaceIdWithText(val, getNameById); | |||
| }); | |||
| return ret; | |||
| } | |||
| return obj; | |||
| }; | |||