F: Implementing M-Graph pt3

This commit is contained in:
Ivan 2024-11-14 22:10:29 +03:00
parent 178499676c
commit 29b169bef4
17 changed files with 774 additions and 355 deletions

File diff suppressed because it is too large Load Diff

View File

@ -19,45 +19,45 @@
"axios": "^1.7.7", "axios": "^1.7.7",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-hooks": "^5.0.0",
"framer-motion": "^11.11.10", "framer-motion": "^11.11.11",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-error-boundary": "^4.1.2", "react-error-boundary": "^4.1.2",
"react-icons": "^5.3.0", "react-icons": "^5.3.0",
"react-intl": "^6.8.4", "react-intl": "^6.8.7",
"react-loader-spinner": "^6.1.6", "react-loader-spinner": "^6.1.6",
"react-router-dom": "^6.27.0", "react-router-dom": "^6.28.0",
"react-select": "^5.8.2", "react-select": "^5.8.2",
"react-tabs": "^6.0.2", "react-tabs": "^6.0.2",
"react-toastify": "^10.0.6", "react-toastify": "^10.0.6",
"react-tooltip": "^5.28.0", "react-tooltip": "^5.28.0",
"react-zoom-pan-pinch": "^3.6.1", "react-zoom-pan-pinch": "^3.6.1",
"reactflow": "^11.11.4", "reactflow": "^11.11.4",
"reagraph": "^4.19.4", "reagraph": "^4.19.5",
"use-debounce": "^10.0.4" "use-debounce": "^10.0.4"
}, },
"devDependencies": { "devDependencies": {
"@lezer/generator": "^1.7.1", "@lezer/generator": "^1.7.1",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "^22.8.1", "@types/node": "^22.9.0",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^8.0.1", "@typescript-eslint/eslint-plugin": "^8.0.1",
"@typescript-eslint/parser": "^8.0.1", "@typescript-eslint/parser": "^8.0.1",
"@vitejs/plugin-react": "^4.3.3", "@vitejs/plugin-react": "^4.3.3",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"eslint": "^9.13.0", "eslint": "^9.14.0",
"eslint-plugin-react": "^7.37.2", "eslint-plugin-react": "^7.37.2",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^15.11.0", "globals": "^15.12.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"tailwindcss": "^3.4.14", "tailwindcss": "^3.4.14",
"ts-jest": "^29.2.5", "ts-jest": "^29.2.5",
"typescript": "^5.6.3", "typescript": "^5.6.3",
"typescript-eslint": "^8.11.0", "typescript-eslint": "^8.13.0",
"vite": "^5.4.10" "vite": "^5.4.10"
}, },
"jest": { "jest": {

View File

@ -1,15 +1,35 @@
'use client'; 'use client';
import { useMemo } from 'react';
import { toast } from 'react-toastify';
import { ReactFlowProvider } from 'reactflow';
import Modal, { ModalProps } from '@/components/ui/Modal'; import Modal, { ModalProps } from '@/components/ui/Modal';
import { IArgumentInfo } from '@/models/rslang'; import { IArgumentInfo } from '@/models/rslang';
import { TMGraph } from '@/models/TMGraph';
import { errors } from '@/utils/labels';
import MGraphFlow from './MGraphFlow';
interface DlgShowTypificationProps extends Pick<ModalProps, 'hideWindow'> { interface DlgShowTypificationProps extends Pick<ModalProps, 'hideWindow'> {
result: string; alias: string;
resultTypification: string;
args: IArgumentInfo[]; args: IArgumentInfo[];
} }
function DlgShowTypification({ hideWindow, result, args }: DlgShowTypificationProps) { function DlgShowTypification({ hideWindow, alias, resultTypification, args }: DlgShowTypificationProps) {
console.log(result, args); const graph = useMemo(() => {
const result = new TMGraph();
result.addConstituenta(alias, resultTypification, args);
return result;
}, [alias, resultTypification, args]);
if (graph.nodes.length === 0) {
toast.error(errors.typeStructureFailed);
hideWindow();
return null;
}
return ( return (
<Modal <Modal
header='Структура типизации' header='Структура типизации'
@ -17,7 +37,9 @@ function DlgShowTypification({ hideWindow, result, args }: DlgShowTypificationPr
hideWindow={hideWindow} hideWindow={hideWindow}
className='flex flex-col justify-stretch w-[calc(100dvw-3rem)] h-[calc(100dvh-6rem)]' className='flex flex-col justify-stretch w-[calc(100dvw-3rem)] h-[calc(100dvh-6rem)]'
> >
<div>В разработке...</div> <ReactFlowProvider>
<MGraphFlow data={graph} />
</ReactFlowProvider>
</Modal> </Modal>
); );
} }

View File

@ -0,0 +1,77 @@
'use client';
import { useLayoutEffect } from 'react';
import { Edge, ReactFlow, useEdgesState, useNodesState, useReactFlow } from 'reactflow';
import { TMGraph } from '@/models/TMGraph';
import { PARAMETER } from '@/utils/constants';
import { TMGraphEdgeTypes } from './graph/MGraphEdgeTypes';
import { applyLayout } from './graph/MGraphLayout';
import { TMGraphNodeTypes } from './graph/MGraphNodeTypes';
interface MGraphFlowProps {
data: TMGraph;
}
function MGraphFlow({ data }: MGraphFlowProps) {
const [nodes, setNodes] = useNodesState([]);
const [edges, setEdges] = useEdgesState([]);
const flow = useReactFlow();
useLayoutEffect(() => {
const newNodes = data.nodes.map(node => ({
id: String(node.id),
data: node,
position: { x: 0, y: 0 },
type: 'step'
}));
const newEdges: Edge[] = [];
data.nodes.forEach(node => {
const visited = new Set<number>();
const edges = new Map<number, number>();
node.parents.forEach((parent, index) => {
if (visited.has(parent)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
newEdges.at(edges.get(parent)!)!.data!.indices.push(index + 1);
} else {
newEdges.push({
id: String(newEdges.length),
data: node.parents.length > 1 ? { indices: [index + 1] } : undefined,
source: String(parent),
target: String(node.id),
type: node.parents.length > 1 ? 'cartesian' : 'boolean'
});
edges.set(parent, newEdges.length - 1);
visited.add(parent);
}
});
});
applyLayout(newNodes);
setNodes(newNodes);
setEdges(newEdges);
flow.fitView({ duration: PARAMETER.zoomDuration });
}, [data, setNodes, setEdges, flow]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
edgesFocusable={false}
nodesFocusable={false}
nodeTypes={TMGraphNodeTypes}
edgeTypes={TMGraphEdgeTypes}
fitView
maxZoom={2}
minZoom={0.5}
nodesConnectable={false}
snapToGrid={true}
snapGrid={[PARAMETER.ossGridSize, PARAMETER.ossGridSize]}
/>
);
}
export default MGraphFlow;

View File

@ -0,0 +1,13 @@
import { StraightEdge } from 'reactflow';
import { MGraphEdgeInternal } from '@/models/miscellaneous';
function BooleanEdge(props: MGraphEdgeInternal) {
return (
<>
<StraightEdge {...props} />
</>
);
}
export default BooleanEdge;

View File

@ -0,0 +1,20 @@
import { SimpleBezierEdge } from 'reactflow';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { MGraphEdgeInternal } from '@/models/miscellaneous';
function CartesianEdge({ data, ...restProps }: MGraphEdgeInternal) {
const { colors } = useConceptOptions();
return (
<>
<SimpleBezierEdge
{...restProps}
label={data?.indices.join(', ')}
labelBgStyle={{ fill: colors.bgDefault }}
labelStyle={{ fill: colors.fgDefault }}
/>
</>
);
}
export default CartesianEdge;

View File

@ -0,0 +1,9 @@
import { EdgeTypes } from 'reactflow';
import BooleanEdge from './BooleanEdge';
import CartesianEdge from './CartesianEdge';
export const TMGraphEdgeTypes: EdgeTypes = {
boolean: BooleanEdge,
cartesian: CartesianEdge
};

View File

@ -0,0 +1,171 @@
import { Node } from 'reactflow';
import { Graph } from '@/models/Graph';
import { TMGraphNode } from '@/models/TMGraph';
export function applyLayout(nodes: Node<TMGraphNode>[]) {
new LayoutManager(nodes).execute();
}
const UNIT_HEIGHT = 100;
const UNIT_WIDTH = 100;
const MIN_NODE_DISTANCE = 80;
const LAYOUT_ITERATIONS = 8;
class LayoutManager {
nodes: Node<TMGraphNode>[];
graph = new Graph();
ranks = new Map<number, number>();
posX = new Map<number, number>();
posY = new Map<number, number>();
maxRank = 0;
virtualCount = 0;
layers: number[][] = [];
constructor(nodes: Node<TMGraphNode>[]) {
this.nodes = nodes;
this.prepareGraph();
}
/** Prepares graph for layout calculations.
*
* Assumes that nodes are already topologically sorted.
* 1. Adds nodes to graph.
* 2. Adds elementary edges to graph.
* 3. Splits non-elementary edges via virtual nodes.
*/
private prepareGraph(): void {
this.nodes.forEach(node => {
if (this.maxRank < node.data.rank) {
this.maxRank = node.data.rank;
}
const nodeID = node.data.id;
this.ranks.set(nodeID, node.data.rank);
this.graph.addNode(nodeID);
if (node.data.parents.length === 0) {
return;
}
const visited = new Set<number>();
node.data.parents.forEach(parent => {
if (!visited.has(parent)) {
visited.add(parent);
let target = nodeID;
let currentRank = node.data.rank;
const parentRank = this.ranks.get(parent)!;
while (currentRank - 1 > parentRank) {
currentRank = currentRank - 1;
this.virtualCount = this.virtualCount + 1;
this.ranks.set(-this.virtualCount, currentRank);
this.graph.addEdge(-this.virtualCount, target);
target = -this.virtualCount;
}
this.graph.addEdge(parent, target);
}
});
});
}
execute(): void {
this.calculateLayers();
this.calculatePositions();
this.savePositions();
}
private calculateLayers(): void {
this.initLayers();
// TODO: implement ordering algorithm iterations
}
private initLayers(): void {
this.layers = Array.from({ length: this.maxRank + 1 }, () => []);
const visited = new Set<number>();
const dfs = (nodeID: number) => {
if (visited.has(nodeID)) {
return;
}
visited.add(nodeID);
this.layers[this.ranks.get(nodeID)!].push(nodeID);
this.graph.at(nodeID)!.outputs.forEach(dfs);
};
const simpleNodes = this.nodes
.filter(node => node.data.rank === 0)
.sort((a, b) => a.data.text.localeCompare(b.data.text))
.map(node => node.data.id);
simpleNodes.forEach(dfs);
}
private calculatePositions(): void {
this.initPositions();
for (let i = 0; i < LAYOUT_ITERATIONS; i++) {
this.fixLayersPositions();
}
}
private fixLayersPositions(): void {
for (let rank = 1; rank <= this.maxRank; rank++) {
this.layers[rank].reverse().forEach(nodeID => {
const inputs = this.graph.at(nodeID)!.inputs;
const currentPos = this.posX.get(nodeID)!;
if (inputs.length === 1) {
const parent = inputs[0];
const parentPos = this.posX.get(parent)!;
if (currentPos === parentPos) {
return;
}
if (currentPos > parentPos) {
this.tryMoveNodeX(parent, currentPos);
} else {
this.tryMoveNodeX(nodeID, parentPos);
}
} else if (inputs.length % 2 === 1) {
const median = inputs[Math.floor(inputs.length / 2)];
const medianPos = this.posX.get(median)!;
if (currentPos === medianPos) {
return;
}
this.tryMoveNodeX(nodeID, medianPos);
} else {
const median1 = inputs[Math.floor(inputs.length / 2)];
const median2 = inputs[Math.floor(inputs.length / 2) - 1];
const medianPos = (this.posX.get(median1)! + this.posX.get(median2)!) / 2;
this.tryMoveNodeX(nodeID, medianPos);
}
});
}
}
private tryMoveNodeX(nodeID: number, targetX: number) {
const rank = this.ranks.get(nodeID)!;
if (this.layers[rank].some(id => id !== nodeID && Math.abs(targetX - this.posX.get(id)!) < MIN_NODE_DISTANCE)) {
return;
}
this.posX.set(nodeID, targetX);
}
private initPositions(): void {
this.layers.forEach((layer, rank) => {
layer.forEach((nodeID, index) => {
this.posX.set(nodeID, index * UNIT_WIDTH);
this.posY.set(nodeID, -rank * UNIT_HEIGHT);
});
});
}
private savePositions(): void {
this.nodes.forEach(node => {
const nodeID = node.data.id;
node.position = {
x: this.posX.get(nodeID)!,
y: this.posY.get(nodeID)!
};
});
}
}

View File

@ -0,0 +1,27 @@
import { Handle, Position } from 'reactflow';
import { useConceptOptions } from '@/context/ConceptOptionsContext';
import { MGraphNodeInternal } from '@/models/miscellaneous';
import { colorBgTMGraphNode } from '@/styling/color';
import { globals } from '@/utils/constants';
function MGraphNode(node: MGraphNodeInternal) {
const { colors } = useConceptOptions();
return (
<>
<Handle type='source' position={Position.Top} style={{ opacity: 0 }} />
<div
className='w-full h-full cursor-default flex items-center justify-center rounded-full'
data-tooltip-id={globals.tooltip}
data-tooltip-content={node.data.text}
style={{ backgroundColor: colorBgTMGraphNode(node.data, colors) }}
>
{node.data.rank === 0 ? node.data.text : ''}
</div>
<Handle type='target' position={Position.Bottom} style={{ opacity: 0 }} />
</>
);
}
export default MGraphNode;

View File

@ -0,0 +1,7 @@
import { NodeTypes } from 'reactflow';
import MGraphNode from './MGraphNode';
export const TMGraphNodeTypes: NodeTypes = {
step: MGraphNode
};

View File

@ -2,6 +2,8 @@
* Module: Multi-graph for typifications. * Module: Multi-graph for typifications.
*/ */
import { PARAMETER } from '@/utils/constants';
import { IArgumentInfo } from './rslang'; import { IArgumentInfo } from './rslang';
/** /**
@ -75,7 +77,7 @@ export class TMGraph {
const text = parentNode.parents.length === 1 ? `${parentNode.text}` : `(${parentNode.text})`; const text = parentNode.parents.length === 1 ? `${parentNode.text}` : `(${parentNode.text})`;
const node: TMGraphNode = { const node: TMGraphNode = {
id: this.nodes.length, id: this.nodes.length,
rank: parentNode.rank, rank: parentNode.rank + 1,
text: text, text: text,
parents: [parent], parents: [parent],
annotations: [] annotations: []
@ -132,7 +134,7 @@ export class TMGraph {
} }
private processResult(result: string): TMGraphNode | undefined { private processResult(result: string): TMGraphNode | undefined {
if (!result) { if (!result || result === PARAMETER.logicLabel) {
return undefined; return undefined;
} }
return this.parseToNode(result); return this.parseToNode(result);

View File

@ -2,10 +2,11 @@
* Module: Miscellaneous frontend model types. Future targets for refactoring aimed at extracting modules. * Module: Miscellaneous frontend model types. Future targets for refactoring aimed at extracting modules.
*/ */
import { Node } from 'reactflow'; import { EdgeProps, Node } from 'reactflow';
import { LibraryItemType, LocationHead } from './library'; import { LibraryItemType, LocationHead } from './library';
import { IOperation } from './oss'; import { IOperation } from './oss';
import { TMGraphNode } from './TMGraph';
import { UserID } from './user'; import { UserID } from './user';
/** /**
@ -45,6 +46,24 @@ export interface OssNodeInternal {
yPos: number; yPos: number;
} }
/**
* Represents graph TMGraph node internal data.
*/
export interface MGraphNodeInternal {
id: string;
data: TMGraphNode;
dragging: boolean;
xPos: number;
yPos: number;
}
/**
* Represents graph TMGraph edge internal data.
*/
export interface MGraphEdgeInternal extends EdgeProps {
data?: { indices: number[] };
}
/** /**
* Represents graph node coloring scheme. * Represents graph node coloring scheme.
*/ */

View File

@ -17,8 +17,8 @@ import { useRSForm } from '@/context/RSFormContext';
import DlgShowTypification from '@/dialogs/DlgShowTypification'; import DlgShowTypification from '@/dialogs/DlgShowTypification';
import { ConstituentaID, CstType, IConstituenta, ICstUpdateData } from '@/models/rsform'; import { ConstituentaID, CstType, IConstituenta, ICstUpdateData } from '@/models/rsform';
import { isBaseSet, isBasicConcept, isFunctional } from '@/models/rsformAPI'; import { isBaseSet, isBasicConcept, isFunctional } from '@/models/rsformAPI';
import { ParsingStatus } from '@/models/rslang'; import { IExpressionParse, ParsingStatus } from '@/models/rslang';
import { information, labelCstTypification } from '@/utils/labels'; import { errors, information, labelCstTypification } from '@/utils/labels';
import EditorRSExpression from '../EditorRSExpression'; import EditorRSExpression from '../EditorRSExpression';
import ControlsOverlay from './ControlsOverlay'; import ControlsOverlay from './ControlsOverlay';
@ -59,6 +59,7 @@ function FormConstituenta({
const [convention, setConvention] = useState(''); const [convention, setConvention] = useState('');
const [typification, setTypification] = useState('N/A'); const [typification, setTypification] = useState('N/A');
const [showTypification, setShowTypification] = useState(false); const [showTypification, setShowTypification] = useState(false);
const [localParse, setLocalParse] = useState<IExpressionParse | undefined>(undefined);
const [forceComment, setForceComment] = useState(false); const [forceComment, setForceComment] = useState(false);
@ -102,6 +103,7 @@ function FormConstituenta({
setExpression(state.definition_formal || ''); setExpression(state.definition_formal || '');
setTypification(state ? labelCstTypification(state) : 'N/A'); setTypification(state ? labelCstTypification(state) : 'N/A');
setForceComment(false); setForceComment(false);
setLocalParse(undefined);
} }
}, [state, schema, toggleReset]); }, [state, schema, toggleReset]);
@ -132,7 +134,8 @@ function FormConstituenta({
} }
function handleTypificationClick(event: CProps.EventMouse) { function handleTypificationClick(event: CProps.EventMouse) {
if ((!event.ctrlKey && !event.metaKey) || !state || state.parse.status !== ParsingStatus.VERIFIED) { if (!state || (localParse && !localParse.parseResult) || state.parse.status !== ParsingStatus.VERIFIED) {
toast.error(errors.typeStructureFailed);
return; return;
} }
event.stopPropagation(); event.stopPropagation();
@ -145,8 +148,9 @@ function FormConstituenta({
<AnimatePresence> <AnimatePresence>
{showTypification && state ? ( {showTypification && state ? (
<DlgShowTypification <DlgShowTypification
result={state.parse.typification} alias={state.alias}
args={state.parse.args} resultTypification={localParse ? localParse.typification : state.parse.typification}
args={localParse ? localParse.args : state.parse.args}
hideWindow={() => setShowTypification(false)} hideWindow={() => setShowTypification(false)}
/> />
) : null} ) : null}
@ -186,8 +190,9 @@ function FormConstituenta({
noOutline noOutline
readOnly readOnly
label='Типизация' label='Типизация'
title='Отобразить структуру типизации'
value={typification} value={typification}
colors='clr-app clr-text-default cursor-default' colors='clr-app clr-text-default cursor-pointer'
onClick={event => handleTypificationClick(event)} onClick={event => handleTypificationClick(event)}
/> />
) : null} ) : null}
@ -216,6 +221,7 @@ function FormConstituenta({
toggleReset={toggleReset} toggleReset={toggleReset}
onChange={newValue => setExpression(newValue)} onChange={newValue => setExpression(newValue)}
setTypification={setTypification} setTypification={setTypification}
setLocalParse={setLocalParse}
onOpenEdit={onOpenEdit} onOpenEdit={onOpenEdit}
/> />
</AnimateFade> </AnimateFade>

View File

@ -2,7 +2,7 @@
import { ReactCodeMirrorRef } from '@uiw/react-codemirror'; import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { AnimatePresence } from 'framer-motion'; import { AnimatePresence } from 'framer-motion';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import BadgeHelp from '@/components/info/BadgeHelp'; import BadgeHelp from '@/components/info/BadgeHelp';
@ -40,6 +40,7 @@ interface EditorRSExpressionProps {
toggleReset?: boolean; toggleReset?: boolean;
setTypification: (typification: string) => void; setTypification: (typification: string) => void;
setLocalParse: React.Dispatch<React.SetStateAction<IExpressionParse | undefined>>;
onChange: (newValue: string) => void; onChange: (newValue: string) => void;
onOpenEdit?: (cstID: ConstituentaID) => void; onOpenEdit?: (cstID: ConstituentaID) => void;
} }
@ -50,6 +51,7 @@ function EditorRSExpression({
value, value,
toggleReset, toggleReset,
setTypification, setTypification,
setLocalParse,
onChange, onChange,
onOpenEdit, onOpenEdit,
...restProps ...restProps
@ -78,6 +80,7 @@ function EditorRSExpression({
function handleCheckExpression(callback?: (parse: IExpressionParse) => void) { function handleCheckExpression(callback?: (parse: IExpressionParse) => void) {
parser.checkConstituenta(value, activeCst, parse => { parser.checkConstituenta(value, activeCst, parse => {
setLocalParse(parse);
if (parse.errors.length > 0) { if (parse.errors.length > 0) {
onShowError(parse.errors[0], parse.prefixLen); onShowError(parse.errors[0], parse.prefixLen);
} else { } else {
@ -137,7 +140,6 @@ function EditorRSExpression({
toast.error(errors.astFailed); toast.error(errors.astFailed);
} else { } else {
setSyntaxTree(parse.ast); setSyntaxTree(parse.ast);
// TODO: return prefix from parser API instead of prefixLength
setExpression(getDefinitionPrefix(activeCst) + value); setExpression(getDefinitionPrefix(activeCst) + value);
setShowAST(true); setShowAST(true);
} }

View File

@ -6,6 +6,7 @@ import { GramData, Grammeme, NounGrams, PartOfSpeech, VerbGrams } from '@/models
import { GraphColoring } from '@/models/miscellaneous'; import { GraphColoring } from '@/models/miscellaneous';
import { CstClass, ExpressionStatus, IConstituenta } from '@/models/rsform'; import { CstClass, ExpressionStatus, IConstituenta } from '@/models/rsform';
import { ISyntaxTreeNode, TokenID } from '@/models/rslang'; import { ISyntaxTreeNode, TokenID } from '@/models/rslang';
import { TMGraphNode } from '@/models/TMGraph';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
/** /**
@ -563,3 +564,16 @@ export function colorBgGraphNode(cst: IConstituenta, coloringScheme: GraphColori
} }
return ''; return '';
} }
/**
* Determines m-graph color for {@link TMGraphNode}.
*/
export function colorBgTMGraphNode(node: TMGraphNode, colors: IColorTheme): string {
if (node.rank === 0) {
return colors.bgControls;
}
if (node.parents.length === 1) {
return colors.bgTeal;
}
return colors.bgOrange;
}

View File

@ -66,9 +66,15 @@
cursor: default; cursor: default;
} }
.react-flow__edge {
cursor: default;
}
.react-flow__attribution { .react-flow__attribution {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
font-family: var(--font-ui); font-family: var(--font-ui);
margin-left: 3px;
margin-right: 3px;
background-color: transparent; background-color: transparent;
color: var(--cl-fg-60); color: var(--cl-fg-60);
@ -77,19 +83,12 @@
} }
} }
.react-flow__node-input, [class*='react-flow__node-'] {
.react-flow__node-synthesis {
cursor: pointer;
border: 1px solid;
padding: 2px;
width: 150px;
height: 40px;
font-size: 14px; font-size: 14px;
border-radius: 5px; border: 1px solid;
background-color: var(--cl-bg-120);
background-color: var(--cl-bg-120);
color: var(--cl-fg-100); color: var(--cl-fg-100);
border-color: var(--cl-bg-40); border-color: var(--cl-bg-40);
background-color: var(--cl-bg-120); background-color: var(--cl-bg-120);
@ -116,3 +115,21 @@
} }
} }
} }
.react-flow__node-input,
.react-flow__node-synthesis {
cursor: pointer;
border-radius: 5px;
padding: 2px;
width: 150px;
height: 40px;
}
.react-flow__node-step {
cursor: default;
border-radius: 100%;
width: 40px;
height: 40px;
}

View File

@ -22,6 +22,8 @@ import {
} from '@/models/rslang'; } from '@/models/rslang';
import { UserLevel } from '@/models/user'; import { UserLevel } from '@/models/user';
import { PARAMETER } from './constants';
/** /**
* Remove html tags from target string. * Remove html tags from target string.
*/ */
@ -531,7 +533,7 @@ export function labelTypification({
if (!isValid) { if (!isValid) {
return 'N/A'; return 'N/A';
} }
if (resultType === '' || resultType === 'LOGIC') { if (resultType === '' || resultType === PARAMETER.logicLabel) {
resultType = 'Logical'; resultType = 'Logical';
} }
if (args.length === 0) { if (args.length === 0) {
@ -1000,6 +1002,7 @@ export const information = {
*/ */
export const errors = { export const errors = {
astFailed: 'Невозможно построить дерево разбора', astFailed: 'Невозможно построить дерево разбора',
typeStructureFailed: 'Структура отсутствует',
passwordsMismatch: 'Пароли не совпадают', passwordsMismatch: 'Пароли не совпадают',
imageFailed: 'Ошибка при создании изображения', imageFailed: 'Ошибка при создании изображения',
reuseOriginal: 'Повторное использование удаляемой конституенты при отождествлении', reuseOriginal: 'Повторное использование удаляемой конституенты при отождествлении',