### 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
| "react-pdf-highlighter": "^6.1.0", | "react-pdf-highlighter": "^6.1.0", | ||||
| "react-string-replace": "^1.1.1", | "react-string-replace": "^1.1.1", | ||||
| "react-syntax-highlighter": "^15.5.0", | "react-syntax-highlighter": "^15.5.0", | ||||
| "react18-json-view": "^0.2.8", | |||||
| "reactflow": "^11.11.2", | "reactflow": "^11.11.2", | ||||
| "recharts": "^2.12.4", | "recharts": "^2.12.4", | ||||
| "remark-gfm": "^4.0.0", | "remark-gfm": "^4.0.0", | ||||
| "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" | "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" | ||||
| }, | }, | ||||
| "node_modules/@types/lodash": { | "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 | "dev": true | ||||
| }, | }, | ||||
| "node_modules/@types/mdast": { | "node_modules/@types/mdast": { | ||||
| "react-dom": ">=16.6.0" | "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": { | "node_modules/reactcss": { | ||||
| "version": "1.2.3", | "version": "1.2.3", | ||||
| "resolved": "https://registry.npmmirror.com/reactcss/-/reactcss-1.2.3.tgz", | "resolved": "https://registry.npmmirror.com/reactcss/-/reactcss-1.2.3.tgz", | 
| "react-pdf-highlighter": "^6.1.0", | "react-pdf-highlighter": "^6.1.0", | ||||
| "react-string-replace": "^1.1.1", | "react-string-replace": "^1.1.1", | ||||
| "react-syntax-highlighter": "^15.5.0", | "react-syntax-highlighter": "^15.5.0", | ||||
| "react18-json-view": "^0.2.8", | |||||
| "reactflow": "^11.11.2", | "reactflow": "^11.11.2", | ||||
| "recharts": "^2.12.4", | "recharts": "^2.12.4", | ||||
| "remark-gfm": "^4.0.0", | "remark-gfm": "^4.0.0", | 
| import CategorizeHandle from './categorize-handle'; | import CategorizeHandle from './categorize-handle'; | ||||
| import NodeDropdown from './dropdown'; | import NodeDropdown from './dropdown'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import NodePopover from './popover'; | |||||
| export function CategorizeNode({ id, data, selected }: NodeProps<NodeData>) { | export function CategorizeNode({ id, data, selected }: NodeProps<NodeData>) { | ||||
| const categoryData = get(data, 'form.category_description') ?? {}; | const categoryData = get(data, 'form.category_description') ?? {}; | ||||
| const style = operatorMap[data.label as Operator]; | const style = operatorMap[data.label as Operator]; | ||||
| const { t } = useTranslate('flow'); | const { t } = useTranslate('flow'); | ||||
| return ( | 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> | ||||
| </section> | |||||
| </NodePopover> | |||||
| ); | ); | ||||
| } | } | 
| import OperatorIcon from '../../operator-icon'; | import OperatorIcon from '../../operator-icon'; | ||||
| import NodeDropdown from './dropdown'; | import NodeDropdown from './dropdown'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import NodePopover from './popover'; | |||||
| export function RagNode({ | export function RagNode({ | ||||
| id, | id, | ||||
| }: NodeProps<NodeData>) { | }: NodeProps<NodeData>) { | ||||
| const style = operatorMap[data.label as Operator]; | const style = operatorMap[data.label as Operator]; | ||||
| const { t } = useTranslate('flow'); | const { t } = useTranslate('flow'); | ||||
| return ( | 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> | ||||
| </section> | |||||
| </NodePopover> | |||||
| ); | ); | ||||
| } | } | 
| 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; | 
| import CategorizeHandle from './categorize-handle'; | import CategorizeHandle from './categorize-handle'; | ||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| import NodePopover from './popover'; | |||||
| export function RelevantNode({ id, data, selected }: NodeProps<NodeData>) { | export function RelevantNode({ id, data, selected }: NodeProps<NodeData>) { | ||||
| const style = operatorMap[data.label as Operator]; | const style = operatorMap[data.label as Operator]; | ||||
| const { t } = useTranslate('flow'); | const { t } = useTranslate('flow'); | ||||
| return ( | 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> | ||||
| </section> | |||||
| </NodePopover> | |||||
| ); | ); | ||||
| } | } | 
| initialRewriteQuestionValues, | initialRewriteQuestionValues, | ||||
| } from './constant'; | } from './constant'; | ||||
| import useGraphStore, { RFState } from './store'; | import useGraphStore, { RFState } from './store'; | ||||
| import { buildDslComponentsByGraph, receiveMessageError } from './utils'; | |||||
| import { | |||||
| buildDslComponentsByGraph, | |||||
| receiveMessageError, | |||||
| replaceIdWithText, | |||||
| } from './utils'; | |||||
| const selector = (state: RFState) => ({ | const selector = (state: RFState) => ({ | ||||
| nodes: state.nodes, | nodes: state.nodes, | ||||
| return handleRun; | 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); | |||||
| }; | 
| import { humanId } from 'human-id'; | import { humanId } from 'human-id'; | ||||
| import { curry } from 'lodash'; | import { curry } from 'lodash'; | ||||
| import pipe from 'lodash/fp/pipe'; | import pipe from 'lodash/fp/pipe'; | ||||
| import isObject from 'lodash/isObject'; | |||||
| import { Edge, Node, Position } from 'reactflow'; | import { Edge, Node, Position } from 'reactflow'; | ||||
| import { v4 as uuidv4 } from 'uuid'; | import { v4 as uuidv4 } from 'uuid'; | ||||
| import { NodeMap, Operator } from './constant'; | import { NodeMap, Operator } from './constant'; | ||||
| export const receiveMessageError = (res: any) => | export const receiveMessageError = (res: any) => | ||||
| res && (res?.response.status !== 200 || res?.data?.retcode !== 0); | 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; | |||||
| }; |