synthesis

This commit is contained in:
khadanovichba 2024-06-21 18:47:46 +03:00
parent 2f08cd4803
commit 73bbde3acb
30 changed files with 1358 additions and 537 deletions

View File

@ -0,0 +1,69 @@
# Generated by Django 5.0.5 on 2024-06-21 15:47
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rsform', '0007_location_and_flags'),
]
operations = [
migrations.CreateModel(
name='InputNode',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('vertical_coordinate', models.IntegerField(verbose_name='Вертикальная координата звена')),
('horizontal_coordinate', models.IntegerField(verbose_name='Горизонтальная координата звена')),
('rsform_id', models.IntegerField(null=True, verbose_name='Схема')),
],
),
migrations.CreateModel(
name='SynthesisGraph',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('Draft', 'Draft'), ('Completed', 'Completed'), ('Warning', 'Warning'), ('Failed', 'Failed')], max_length=20, verbose_name='Статус операции слияния')),
],
),
migrations.CreateModel(
name='OperationNode',
fields=[
('inputnode_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='rsform.inputnode')),
('name', models.CharField(max_length=20, verbose_name='Название')),
('status', models.CharField(choices=[('Draft', 'Draft'), ('Completed', 'Completed'), ('Warning', 'Warning'), ('Failed', 'Failed')], max_length=20, verbose_name='Статус операции слияния')),
('left_parent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rsform_library_item_left', to='rsform.libraryitem', verbose_name='Левый предок')),
('right_parent', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='rsform_library_item_right', to='rsform.libraryitem', verbose_name='Правый предок')),
],
bases=('rsform.inputnode',),
),
migrations.CreateModel(
name='SynthesisEdge',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('decoded_id', models.CharField(max_length=30, verbose_name='Id ребра на фронте')),
('source_handle', models.CharField(max_length=30, verbose_name='')),
('node_from', models.IntegerField(verbose_name='Звено-предок')),
('node_to', models.IntegerField(verbose_name='Звено-наследник')),
('graph_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.synthesisgraph', verbose_name='Схема синтеза')),
],
),
migrations.AddField(
model_name='inputnode',
name='graph_id',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.synthesisgraph', verbose_name='Схема синтеза'),
),
migrations.CreateModel(
name='SynthesisSubstitution',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('deleteRight', models.BooleanField(verbose_name='Удалить правую')),
('takeLeftTerm', models.BooleanField(verbose_name='Использовать термин левой')),
('graph_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.synthesisgraph', verbose_name='Схема синтеза')),
('leftCst', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='constituenta_original', to='rsform.constituenta', verbose_name='Конституента')),
('rightCst', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='constituenta_substitution', to='rsform.constituenta', verbose_name='Подстановка')),
('operation_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rsform.operationnode', verbose_name='Операция синтеза')),
],
),
]

View File

@ -102,10 +102,6 @@ class LibraryItem(Model):
auto_now=True auto_now=True
) )
#is_hidden: BooleanField = BooleanField(
# default=False
#)
class Meta: class Meta:
''' Model metadata. ''' ''' Model metadata. '''
verbose_name = 'Схема' verbose_name = 'Схема'

View File

@ -1,90 +1,139 @@
from django.db.models import ( from django.db.models import (
CASCADE, SET_NULL, ForeignKey, Model, PositiveIntegerField, QuerySet, CASCADE, SET_NULL, ForeignKey, Model, PositiveIntegerField, QuerySet,
TextChoices, TextField, BooleanField, CharField, DateTimeField, JSONField, IntegerField TextChoices, TextField, BooleanField, CharField, DateTimeField, JSONField, IntegerField, AutoField
) )
from rsconcept.backend.apps.rsform.models.api_RSForm import RSForm, LibraryItem, LibraryItemType
import rsconcept.backend.apps.rsform.messages as messages
class GraphStatus(TextChoices):
DRAFT = 'Draft',
IN_PROGRESS = 'In progress',
COMPLETED = 'Completed',
FAILED = 'Failed'
class OperationStatus(TextChoices): class OperationStatus(TextChoices):
DRAFT = 'Draft', DRAFT = 'Draft',
# IN_PROGRESS = 'In progress',
COMPLETED = 'Completed', COMPLETED = 'Completed',
WARNING = 'Warning', WARNING = 'Warning',
FAILED = 'Failed' FAILED = 'Failed'
class SynthesisNodeType: class GraphStatus(TextChoices):
LIBRARY = 'Library', DRAFT = 'Draft',
SYNTHESIZED = 'Synthesized' COMPLETED = 'Completed',
WARNING = 'Warning',
FAILED = 'Failed'
class ConceptOperation(Model): class SynthesisGraph(Model):
status: CharField = CharField(
verbose_name='Статус операции слияния',
max_length=20,
choices=GraphStatus
)
class InputNode(Model):
graph_id: ForeignKey = ForeignKey(
verbose_name='Схема синтеза',
to=SynthesisGraph,
on_delete=CASCADE
)
vertical_coordinate: IntegerField = IntegerField(
verbose_name='Вертикальная координата звена',
)
horizontal_coordinate: IntegerField = IntegerField(
verbose_name='Горизонтальная координата звена',
)
rsform_id: IntegerField = IntegerField(
verbose_name='Схема',
null=True
)
class OperationNode(InputNode):
name: CharField = CharField( name: CharField = CharField(
verbose_name='Название', verbose_name='Название',
max_length=20 max_length=20
) )
node_type: CharField = CharField( status: CharField = CharField(
verbose_name='Тип звена операции слияния',
max_length=20,
choices=SynthesisNodeType
)
status: CharField(
verbose_name='Статус операции слияния', verbose_name='Статус операции слияния',
max_length=20, max_length=20,
choices=OperationStatus choices=OperationStatus
) )
vertical_coordinate = IntegerField( left_parent: ForeignKey = ForeignKey(
verbose_name='Вертикальная координата звена', verbose_name='Левый предок',
to='rsform.LibraryItem',
related_name='rsform_library_item_left',
on_delete=SET_NULL,
null=True
) )
horizontal_coordinate = IntegerField( right_parent: ForeignKey = ForeignKey(
verbose_name='Горизонтальная координата звена', verbose_name='Правый предок',
to='rsform.LibraryItem',
related_name='rsform_library_item_right',
on_delete=SET_NULL,
null=True
) )
rsform = ForeignKey(
verbose_name='Схема', class SynthesisSubstitution(Model):
to='rsform.LibraryItem' graph_id: ForeignKey = ForeignKey(
verbose_name='Схема синтеза',
to=SynthesisGraph,
on_delete=CASCADE
) )
operation_type = CharField() operation_id: ForeignKey = ForeignKey(
verbose_name='Операция синтеза',
to=OperationNode,
class SynthesisGraph(Model): on_delete=CASCADE
name: CharField = CharField(
verbose_name='Название',
max_length=20
) )
status: CharField = CharField(
verbose_name='Статус схемы слияния', leftCst: ForeignKey = ForeignKey(
max_length=20, verbose_name='Конституента',
choices=GraphStatus, to='Constituenta',
related_name='constituenta_original',
on_delete=SET_NULL,
null=True
)
rightCst: ForeignKey = ForeignKey(
verbose_name='Подстановка',
to='Constituenta',
related_name='constituenta_substitution',
on_delete=SET_NULL,
null=True
)
deleteRight: BooleanField = BooleanField(
verbose_name='Удалить правую'
)
takeLeftTerm: BooleanField = BooleanField(
verbose_name='Использовать термин левой'
) )
class SynthesisEdge(Model): class SynthesisEdge(Model):
synthesis_graph: ForeignKey( decoded_id: CharField = CharField(
verbose_name='Id ребра на фронте',
max_length=30,
)
source_handle: CharField = CharField(
verbose_name='',
max_length=30,
)
graph_id: ForeignKey = ForeignKey(
verbose_name='Схема синтеза', verbose_name='Схема синтеза',
to=SynthesisGraph, to=SynthesisGraph,
on_delete=CASCADE
) )
node_from: ForeignKey( node_from: IntegerField = IntegerField(
verbose_name='Звено-предок', verbose_name='Звено-предок',
to='rsform.LibraryItem'
) )
node_to: ForeignKey( node_to: IntegerField = IntegerField(
verbose_name='Звено-наследник', verbose_name='Звено-наследник',
to='rsform.LibraryItem'
) )

View File

@ -1,18 +1,112 @@
from rest_framework import serializers from rest_framework import serializers
from .data_access import InlineSynthesisSerializer from .data_access import CstSubstituteSerializerBase
from rest_framework.serializers import PrimaryKeyRelatedField as PKField
class SynthesisEdgeSerializer(serializers.Serializer): from ..models import Constituenta, LibraryItem
schema_from = serializers.IntegerField() from ..models.Synthesis import SynthesisGraph, SynthesisEdge, InputNode, OperationNode, SynthesisSubstitution
schema_to = serializers.IntegerField()
class SynthesisNodeSerializer(serializers.Serializer): class SynthesisGraphSerializer(serializers.ModelSerializer):
pk = serializers.IntegerField() class Meta:
vertical_coordinate = serializers.IntegerField() model = SynthesisGraph
horizontal_coordinate = serializers.IntegerField() fields = '__all__'
def create(self, validated_data):
graph, created = SynthesisGraph.objects.update_or_create(
id=validated_data['id'],
defaults={'status': validated_data['status']}
)
return graph
class SynthesisGraphSerializer(serializers.Serializer): class InputNodeSerializer(serializers.ModelSerializer):
pk = serializers.IntegerField() class Meta:
user = serializers.CharField() model = InputNode
edges_list = serializers.ListField(child=SynthesisEdgeSerializer()) fields = '__all__'
def create(self, validated_data_list):
for validated_data in validated_data_list:
input_node, created = InputNode.objects.update_or_create(
id=validated_data['id'] if validated_data.get('id') else None,
defaults={
'graph_id': validated_data['graph_id'],
'vertical_coordinate': validated_data['vertical_coordinate'],
'horizontal_coordinate': validated_data['horizontal_coordinate'],
'rsform_id': validated_data['rsform_id'],
}
)
return
class OperationNodeSerializer(serializers.ModelSerializer):
class Meta:
model = OperationNode
fields = '__all__'
def create(self, validated_data_list):
operations = []
for validated_data in validated_data_list:
operation_node, created = OperationNode.objects.update_or_create(
id=validated_data['id'],
defaults={
'graph_id': validated_data['graph_id'],
'vertical_coordinate': validated_data['vertical_coordinate'],
'horizontal_coordinate': validated_data['horizontal_coordinate'],
'rsform_id': validated_data['rsform_id'],
'left_parent': validated_data.get('left_parent'),
'right_parent': validated_data.get('right_parent'),
}
)
operations.append(operation_node)
return operations
class SynthesisSubstitutionSerializer(serializers.ModelSerializer):
class Meta:
model = SynthesisSubstitution
fields = '__all__'
def create(self, validated_data_list):
substitutions = []
for validated_data in validated_data_list:
substitution, created = SynthesisSubstitution.objects.update_or_create(
id=validated_data['id'],
defaults={
'operation_id': validated_data['operation_id'],
'graph_id': validated_data['graph_id'],
'leftCst': validated_data['leftCst'],
'rightCst': validated_data['rightCst'],
'deleteRight': validated_data['deleteRight'],
'takeLeftTerm': validated_data['takeLeftTerm'],
}
)
substitutions.append(substitution)
return substitutions
class SynthesisEdgeSerializer(serializers.ModelSerializer):
class Meta:
model = SynthesisEdge
fields = '__all__'
def create(self, validated_data_list):
for validated_data in validated_data_list:
substitution, created = SynthesisEdge.objects.update_or_create(
id=validated_data['id'],
defaults={
'graph_id': validated_data['graph_id'],
'decoded_id': validated_data['decoded_id'],
'source_handle': validated_data['source_handle'],
'node_from': validated_data['node_from'],
'node_to': validated_data['node_to'],
}
)
return
class RunSingleSynthesis(serializers.Serializer):
operationId = serializers.IntegerField()
class RunSingleSynthesisResponse(serializers.Serializer):
rsformId = serializers.IntegerField()

View File

@ -30,7 +30,9 @@ urlpatterns = [
path('cctext/inflect', views.inflect), path('cctext/inflect', views.inflect),
path('cctext/generate-lexeme', views.generate_lexeme), path('cctext/generate-lexeme', views.generate_lexeme),
path('cctext/parse', views.parse_text), path('cctext/parse', views.parse_text),
path('synthesis/single', views.run_synthesis_view), path('synthesis/run_single', views.run_synthesis_view),
path('synthesis/graph', views.run_sythesis_graph_view), path('synthesis/run_all', views.run_sythesis_graph_view),
path('synthesis/save', views.save_synthesis_graph),
path('synthesis/<int:pk_item>', views.get_synthesis_graph),
path('', include(library_router.urls)), path('', include(library_router.urls)),
] ]

View File

@ -9,5 +9,7 @@ from .versions import VersionViewset, create_version, export_file, retrieve_vers
from .synthesis import ( from .synthesis import (
run_synthesis_view, run_synthesis_view,
run_sythesis_graph_view run_sythesis_graph_view,
save_synthesis_graph,
get_synthesis_graph,
) )

View File

@ -5,72 +5,158 @@ from rest_framework import status
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from django.db.models import Q
from rest_framework.views import APIView
from ..models.Constituenta import Constituenta from ..models.Constituenta import Constituenta
from ..models.LibraryItem import LibraryItem from ..models.LibraryItem import LibraryItem
from ..models.api_RSForm import RSForm from ..models.api_RSForm import RSForm
from ..models.Synthesis import SynthesisGraph, InputNode, OperationNode, SynthesisSubstitution, SynthesisEdge
from ..serializers import RSFormSerializer, SynthesisGraphSerializer, InlineSynthesisSerializer from ..serializers import RSFormSerializer, SynthesisGraphSerializer, InlineSynthesisSerializer
from typing import cast from typing import cast
from django.contrib.auth.models import User from django.contrib.auth.models import User
from ..serializers.data_access import CstBaseSerializer, CstSerializer
from ..serializers.synthesis import OperationNodeSerializer, InputNodeSerializer, \
SynthesisSubstitutionSerializer, SynthesisEdgeSerializer, RunSingleSynthesis, RunSingleSynthesisResponse
from ..utils import clone_rsform from ..utils import clone_rsform
@extend_schema( @extend_schema(
summary='Run synthesis operation', summary='Get synthesis graph',
tags=['Synthesis'], tags=['Synthesis'],
request=InlineSynthesisSerializer, auth=None
)
@api_view(['GET'])
def get_synthesis_graph(request: Request, pk_item: int):
input_nodes = InputNode.objects.filter(graph_id=pk_item)
operation_nodes = OperationNode.objects.filter(graph_id=pk_item)
edges = SynthesisEdge.objects.filter(graph_id=pk_item)
substitutions = []
for operation_node in operation_nodes:
substitution_batch = SynthesisSubstitution.objects.filter(operation_id=operation_node.id)
for substitution in substitution_batch:
substitutions.append(substitution)
synthesis_graph = SynthesisGraphSerializer()
synthesis_graph.create(validated_data={'id': pk_item, 'status': 'Draft'})
input_nodes = InputNodeSerializer(instance=input_nodes, many=True)
operation_nodes = (OperationNodeSerializer(instance=operation_nodes, many=True))
edges = SynthesisEdgeSerializer(instance=edges, many=True)
substitutions = SynthesisSubstitutionSerializer(instance=substitutions, many=True)
for substitution in substitutions.data:
substitution['leftCst'] = CstSerializer(instance=Constituenta.objects.get(id=substitution['leftCst'])).data
substitution['rightCst'] = CstSerializer(instance=Constituenta.objects.get(id=substitution['rightCst'])).data
return Response(data={
'graph': synthesis_graph.data,
'input_nodes': input_nodes.data,
'operation_nodes': operation_nodes.data,
'edges': edges.data,
'substitutions': substitutions.data,
})
@extend_schema(
summary='Save synthesis graph',
tags=['Synthesis'],
request=SynthesisGraphSerializer,
responses={status.HTTP_200_OK: RSFormSerializer}, responses={status.HTTP_200_OK: RSFormSerializer},
auth=None auth=None
) )
@api_view(['POST']) @api_view(['POST'])
def run_synthesis_view(request: Request): def save_synthesis_graph(request: Request):
serializer = InlineSynthesisSerializer( graph_data = request.data.get('graph')
data=request.data, input_nodes_data = request.data.get('input_nodes')
context={'user': request.user} operation_nodes_data = request.data.get('operation_nodes')
) edges_data = request.data.get('edges')
serializer.is_valid(raise_exception=True) substitutions_data = request.data.get('substitutions')
return run_synthesis(serializer=serializer)
synthesis_graph_serializer = SynthesisGraphSerializer()
graph = synthesis_graph_serializer.create(validated_data=graph_data)
InputNode.objects.filter(graph_id=graph).delete()
OperationNode.objects.filter(graph_id=graph).delete()
SynthesisEdge.objects.filter(graph_id=graph).delete()
SynthesisSubstitution.objects.filter(graph_id=graph).delete()
input_node_serializer = InputNodeSerializer()
for input_node in input_nodes_data:
input_node['graph_id'] = graph
input_node_serializer.create(validated_data_list=input_nodes_data)
for operation_node in operation_nodes_data:
operation_node['graph_id'] = graph
operation_node['left_parent'] = LibraryItem.objects.get(id=operation_node['left_parent'])
operation_node['right_parent'] = LibraryItem.objects.get(id=operation_node['right_parent'])
operation_node_serializer = OperationNodeSerializer()
operations = operation_node_serializer.create(validated_data_list=operation_nodes_data)
for edge in edges_data:
edge['graph_id'] = graph
edge_serializer = SynthesisEdgeSerializer()
edge_serializer.create(validated_data_list=edges_data)
operations_dict = {operation.id: operation for operation in operations}
for substitution_data in substitutions_data:
substitution_data['operation_id'] = operations_dict[substitution_data['operation_id']]
substitution_data['rightCst'] = Constituenta.objects.get(id=substitution_data['rightCst']['id'])
substitution_data['leftCst'] = Constituenta.objects.get(id=substitution_data['leftCst']['id'])
substitution_data['graph_id'] = graph
substitution_serializer = SynthesisSubstitutionSerializer()
substitutions = substitution_serializer.create(validated_data_list=substitutions_data)
return Response(synthesis_graph_serializer.data, status=status.HTTP_201_CREATED)
@extend_schema( @extend_schema(
summary='Run synthesis graph', summary='Run synthesis graph',
tags=['Synthesis'], tags=['Synthesis'],
request=InlineSynthesisSerializer, request=RunSingleSynthesis,
responses={status.HTTP_200_OK: RSFormSerializer}, responses={status.HTTP_200_OK: RunSingleSynthesisResponse},
auth=None auth=None
) )
@api_view(['POST']) @api_view(['POST'])
def run_sythesis_graph_view(request: Request): def run_sythesis_graph_view(request: Request):
serializer = SynthesisGraphSerializer(data=request.data) serializer = RunSingleSynthesis(data=request.data)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
for atomic_synthesis in serializer.validated_data: for atomic_synthesis in serializer.validated_data:
run_synthesis(atomic_synthesis) run_synthesis(atomic_synthesis)
def run_synthesis(serializer: InlineSynthesisSerializer): @extend_schema(
left_schema = RSForm(serializer.validated_data['source']) summary='Run synthesis operation',
right_schema = RSForm(serializer.validated_data['receiver']) tags=['Synthesis'],
constituents = cast(list[Constituenta], serializer.validated_data['items']) request=RunSingleSynthesis,
responses={status.HTTP_200_OK: RunSingleSynthesisResponse},
auth=None
)
@api_view(['POST'])
def run_synthesis_view(request: Request):
serializer = RunSingleSynthesis(
data=request.data
)
serializer.is_valid(raise_exception=True)
return run_synthesis(serializer=serializer)
def run_synthesis(serializer: RunSingleSynthesis):
operation_id = serializer.data['operationId']
operation = OperationNode.objects.get(id=operation_id)
left_schema = RSForm(operation.left_parent)
right_schema = RSForm(operation.right_parent)
substitutions = SynthesisSubstitution.objects.filter(operation_id=operation_id)
left_schema_copy = clone_rsform(left_schema) left_schema_copy = clone_rsform(left_schema)
copied_constituents = left_schema_copy.item.constituenta_set right_constituents = right_schema.item.constituenta_set.filter()
used_constiuents = set() left_schema_copy.insert_copy(right_constituents)
for substitution in serializer.validated_data['substitutions']: for substitution in substitutions:
original = cast(Constituenta, substitution['original']) original = cast(Constituenta, substitution.leftCst)
replacement = cast(Constituenta, substitution['substitution']) replacement = cast(Constituenta, substitution.rightCst)
if original in constituents: left_schema_copy.substitute(original, replacement, (not substitution.deleteRight) and substitution.takeLeftTerm)
index = next(i for (i, cst) in enumerate(constituents) if cst == original)
original = copied_constituents[index]
else:
index = next(i for (i, cst) in enumerate(constituents) if cst == replacement)
replacement = copied_constituents[index]
left_schema_copy.substitute(original, replacement, substitution['transfer_term'])
if substitution['transfer_term']:
used_constiuents.add(replacement.pk)
unused_constitunents = {cst for cst in right_schema.item.constituenta_set() if cst.pk not in used_constiuents}
left_schema_copy.insert_copy(list(unused_constitunents))
left_schema.restore_order() left_schema.restore_order()
return Response( return Response(
@ -78,7 +164,6 @@ def run_synthesis(serializer: InlineSynthesisSerializer):
data=RSFormSerializer(left_schema_copy.item).data data=RSFormSerializer(left_schema_copy.item).data
) )
return
right_rsform_copy = clone_rsform(right_schema) right_rsform_copy = clone_rsform(right_schema)
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)

15
rsconcept/frontend/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}"
}
]
}

View File

@ -17,7 +17,7 @@ import {
LibraryItemType LibraryItemType
} from '@/models/library'; } from '@/models/library';
import { ILibraryCreateData } from '@/models/library'; import { ILibraryCreateData } from '@/models/library';
import { IOperationSchemaData } from '@/models/oss'; import { IOperationSchemaData, IRunSynthesis, IRunSynthesisResponse } from '@/models/oss';
import { import {
IConstituentaList, IConstituentaList,
IConstituentaMeta, IConstituentaMeta,
@ -51,7 +51,7 @@ import {
IUserUpdatePassword IUserUpdatePassword
} from '@/models/user'; } from '@/models/user';
import { buildConstants } from '@/utils/buildConstants'; import { buildConstants } from '@/utils/buildConstants';
import {ISynthesisData} from "@/models/synthesis.ts"; import { ISynthesisGraphData } from '@/models/oss.ts';
const defaultOptions = { const defaultOptions = {
xsrfCookieName: 'csrftoken', xsrfCookieName: 'csrftoken',
@ -86,6 +86,7 @@ interface IFrontRequest<RequestData, ResponseData> {
export interface FrontPush<DataType> extends IFrontRequest<DataType, undefined> { export interface FrontPush<DataType> extends IFrontRequest<DataType, undefined> {
data: DataType; data: DataType;
} }
export interface FrontPull<DataType> extends IFrontRequest<undefined, DataType> { export interface FrontPull<DataType> extends IFrontRequest<undefined, DataType> {
onSuccess: DataCallback<DataType>; onSuccess: DataCallback<DataType>;
} }
@ -95,7 +96,8 @@ export interface FrontExchange<RequestData, ResponseData> extends IFrontRequest<
onSuccess: DataCallback<ResponseData>; onSuccess: DataCallback<ResponseData>;
} }
export interface FrontAction extends IFrontRequest<undefined, undefined> {} export interface FrontAction extends IFrontRequest<undefined, undefined> {
}
interface IAxiosRequest<RequestData, ResponseData> { interface IAxiosRequest<RequestData, ResponseData> {
endpoint: string; endpoint: string;
@ -235,26 +237,10 @@ export function postCloneLibraryItem(target: string, request: FrontExchange<IRSF
} }
export function getOssDetails(target: string, request: FrontPull<IOperationSchemaData>) { export function getOssDetails(target: string, request: FrontPull<IOperationSchemaData>) {
request.onSuccess({ AxiosGet({
id: Number(target), endpoint: `/api/synthesis/${target}`,
comment: '123', request: request
alias: 'oss1',
access_policy: AccessPolicy.PUBLIC,
editors: [],
owner: 1,
item_type: LibraryItemType.OSS,
location: '/U',
read_only: false,
subscribers: [],
time_create: '0',
time_update: '0',
title: 'TestOss',
visible: false
}); });
// AxiosGet({
// endpoint: `/api/oss/${target}`, // TODO: endpoint to access OSS
// request: request
// });
} }
export function getRSFormDetails(target: string, version: string, request: FrontPull<IRSFormData>) { export function getRSFormDetails(target: string, version: string, request: FrontPull<IRSFormData>) {
@ -438,16 +424,24 @@ export function patchUploadTRS(target: string, request: FrontExchange<IRSFormUpl
} }
}); });
} }
export function patchInlineSynthesis(request: FrontExchange<IInlineSynthesisData, IRSFormData>) {
export function patchInlineSynthesis(request: FrontExchange<ISynthesisGraphData, ISynthesisGraphData>) {
AxiosPatch({ AxiosPatch({
endpoint: `/api/operations/inline-synthesis`, endpoint: `/api/operations/inline-synthesis`,
request: request request: request
}); });
} }
export function postSynthesis(request: FrontExchange<ISynthesisData, IRSFormData>){ export function runSingleSynthesis(request: FrontExchange<IRunSynthesis, IRunSynthesisResponse>) {
AxiosPatch({ AxiosPost({
endpoint: `/api/synthesis/single`, endpoint: `/api/synthesis/run_single`,
request: request
});
}
export function postSynthesisGraph(request: FrontExchange<ISynthesisGraphData, ISynthesisGraphData>) {
AxiosPost({
endpoint: `/api/synthesis/save`,
request: request request: request
}); });
} }
@ -531,7 +525,7 @@ function AxiosPost<RequestData, ResponseData>({
endpoint, endpoint,
request, request,
options options
}: IAxiosRequest<RequestData, ResponseData>) { }: IAxiosRequest<RequestData, ResponseData>) {
if (request.setLoading) request.setLoading(true); if (request.setLoading) request.setLoading(true);
axiosInstance axiosInstance
.post<ResponseData>(endpoint, request.data, options) .post<ResponseData>(endpoint, request.data, options)
@ -550,7 +544,7 @@ function AxiosDelete<RequestData, ResponseData>({
endpoint, endpoint,
request, request,
options options
}: IAxiosRequest<RequestData, ResponseData>) { }: IAxiosRequest<RequestData, ResponseData>) {
if (request.setLoading) request.setLoading(true); if (request.setLoading) request.setLoading(true);
axiosInstance axiosInstance
.delete<ResponseData>(endpoint, options) .delete<ResponseData>(endpoint, options)
@ -569,7 +563,7 @@ function AxiosPatch<RequestData, ResponseData>({
endpoint, endpoint,
request, request,
options options
}: IAxiosRequest<RequestData, ResponseData>) { }: IAxiosRequest<RequestData, ResponseData>) {
if (request.setLoading) request.setLoading(true); if (request.setLoading) request.setLoading(true);
axiosInstance axiosInstance
.patch<ResponseData>(endpoint, request.data, options) .patch<ResponseData>(endpoint, request.data, options)

View File

@ -1,18 +1,55 @@
import {memo, type FC} from 'react'; import { memo, type FC } from 'react';
import {Handle, Position, type NodeProps} from '@reactflow/core'; import { Handle, Position, type NodeProps } from '@reactflow/core';
import Button from '@/components/ui/Button.tsx';
import { PiPlugsConnected } from 'react-icons/pi';
import { CiSquareRemove } from 'react-icons/ci';
import MiniButton from '@/components/ui/MiniButton.tsx';
import { useSynthesis } from '@/pages/OssPage/SynthesisContext.tsx';
interface InputNodeProps {
id: string;
data: {
label: string;
onDelete: (nodeId: string) => void;
};
bound_rsform_id: number;
}
const InputNode: FC<InputNodeProps> = ({ id, data,bound_rsform_id }) => {
const controller = useSynthesis();
const { label, onDelete } = data;
const handleDelete = () => {
onDelete(id);
};
const handleClick = () =>{
controller.selectNode(id);
controller.showSelectInput();
}
const InputNode: FC<NodeProps> = ({data, xPos, yPos}) => {
return ( return (
<> <>
<Handle type="target" position={Position.Bottom}/> <Handle type="target" position={Position.Bottom} />
<div> <div>
<MiniButton className="float-right"
icon={<CiSquareRemove className="icon-red" />}
title="Удалить"
onClick={handleDelete}
color={'red'}
/>
<div> <div>
Тип: <strong>{data.label}</strong> Тип: <strong>Ввод</strong>
</div> </div>
<div> <div>
Схема из библиотеки:{' '} Схема:{controller.getBind(id) === undefined? '': controller.getBind(id)}
<strong> <strong>
RSForm <MiniButton className="float-right"
icon={<PiPlugsConnected className="icon-green" />}
title="Привязать схему"
onClick={() => {handleClick()}}
/>
</strong> </strong>
</div> </div>
</div> </div>

View File

@ -1,25 +1,74 @@
import {memo, type FC, type CSSProperties} from 'react'; import { memo, type FC, type CSSProperties } from 'react';
import {Handle, Position, type NodeProps} from '@reactflow/core'; import { Handle, Position, type NodeProps } from '@reactflow/core';
import MiniButton from '@/components/ui/MiniButton.tsx';
import { IoGitNetworkSharp } from 'react-icons/io5';
import { useSynthesis } from '@/pages/OssPage/SynthesisContext.tsx';
import { CiSquareRemove } from 'react-icons/ci';
import { VscDebugStart } from "react-icons/vsc";
const sourceHandleStyleA: CSSProperties = {left: 50}; const sourceHandleStyleA: CSSProperties = { left: 50 };
const sourceHandleStyleB: CSSProperties = { const sourceHandleStyleB: CSSProperties = {
right: 50, right: 50,
left: 'auto', left: 'auto'
}; };
const OperationNode: FC<NodeProps> = ({data, xPos, yPos}) => { interface OperationNodeProps {
id: string;
data: {
label: string;
onDelete: (nodeId: string) => void;
};
xPos: number,
yPos: number,
}
const OperationNode: FC<OperationNodeProps> = ({ id, data, xPos, yPos }) => {
const controller = useSynthesis();
const { label, onDelete } = data;
const handleDelete = () => {
onDelete(id);
};
const handleSubstitution = () =>{
controller.selectNode(id);
controller.showSynthesis();
}
const handleSynthesis = () => {
controller.singleSynthesis(id)
}
return ( return (
<> <>
<Handle type="target" position={Position.Bottom}/> <Handle type="target" position={Position.Bottom} />
<div> <div>
<MiniButton className="float-right"
icon={<CiSquareRemove className="icon-red" />}
title="Удалить"
onClick={handleDelete}
color={'red'}
/>
<div> <div>
Тип: <strong>{data.label}</strong> Тип: <strong>Отождествление</strong>
</div> </div>
<div> <div>
Схема:{' '} Схема:{' '}
<strong> <strong>
{xPos.toFixed(2)},{yPos.toFixed(2)}
</strong> </strong>
<MiniButton
className="float-right"
icon={<VscDebugStart className="icon-green" />}
title="Синтез"
onClick={() => handleSynthesis()}
/>
<MiniButton
className="float-right"
icon={<IoGitNetworkSharp className="icon-green" />}
title="Отождествления"
onClick={() => handleSubstitution()}
/>
</div> </div>
</div> </div>

View File

@ -3,9 +3,16 @@
font-size: 12px; font-size: 12px;
} }
.react-flow__node-input {
border: 1px solid #555;
padding: 10px;
width: 150px;
border-radius: 5px;
}
.react-flow__node-custom { .react-flow__node-custom {
border: 1px solid #555; border: 1px solid #555;
padding: 10px; padding: 10px;
width: 200px; width: 250px;
border-radius: 5px; border-radius: 5px;
} }

View File

@ -1,4 +1,4 @@
import {useCallback} from 'react'; import { useCallback, useMemo } from 'react';
import { import {
ReactFlow, ReactFlow,
addEdge, addEdge,
@ -6,9 +6,10 @@ import {
useEdgesState, useEdgesState,
type Connection, type Connection,
type Edge, type Edge,
type Node, type Node, OnSelectionChangeParams
} from '@reactflow/core'; } from '@reactflow/core';
import OperationNode from './OperationNode'; import OperationNode from './OperationNode';
import InputNode from './InputNode'; import InputNode from './InputNode';
@ -16,63 +17,33 @@ import InputNode from './InputNode';
import '@reactflow/core/dist/style.css'; import '@reactflow/core/dist/style.css';
import './SynthesisFlow.css'; import './SynthesisFlow.css';
import DlgSynthesis from "@/dialogs/DlgSynthesis.tsx"; import { useState } from 'react';
import { useSynthesis } from '@/pages/OssPage/SynthesisContext.tsx';
import { useConceptOptions } from '@/context/OptionsContext.tsx';
const nodeTypes = { const nodeTypes = {
custom: OperationNode, custom: OperationNode,
input: InputNode, input: InputNode
}; };
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
data: {label: 'Node 1'},
position: {x: 250, y: 5},
},
{
type: 'input',
id: '2',
data: {label: 'Node 2'},
position: {x: 100, y: 100},
},
{
id: '3',
data: {label: 'Node 3'},
position: {x: 400, y: 100},
type: 'custom',
},
{
id: '4',
data: {label: 'Node 4'},
position: {x: 400, y: 200},
type: 'custom',
},
];
const initialEdges: Edge[] = [
//{ id: 'e1-2', source: '1', target: '2', animated: true },
//{ id: 'e1-3', source: '1', target: '3', animated: true },
];
function Flow() { function Flow() {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const controller = useSynthesis();
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const { calculateHeight, darkMode } = useConceptOptions();
const onConnect = useCallback( const canvasWidth = useMemo(() => {
(params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), return 'calc(100vw - 1rem)';
[setEdges] }, []);
);
const canvasHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]);
return ( return (
<div className="Flow" style={{height: 800, width: 1000}}> <div className="relative" style={{ height: canvasHeight, width: canvasWidth }}>
<ReactFlow <ReactFlow
nodes={nodes} nodes={controller.getNodes()}
onNodesChange={onNodesChange} onNodesChange={controller.onNodesChange}
edges={edges} onNodesDelete={controller.onNodesDelete}
onEdgesChange={onEdgesChange} edges={controller.getEdges()}
onConnect={onConnect} onEdgesChange={controller.onEdgesChange}
onConnect={controller.onConnect}
fitView fitView
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
/> />

View File

@ -6,6 +6,7 @@ import { ConstituentaID, IRSForm, ISubstitution } from '@/models/rsform';
import { prefixes } from '@/utils/constants'; import { prefixes } from '@/utils/constants';
import PickSubstitutions from '../../components/select/PickSubstitutions'; import PickSubstitutions from '../../components/select/PickSubstitutions';
import { ISynthesisSubstitution } from '@/models/oss.ts';
interface SubstitutionsTabProps { interface SubstitutionsTabProps {
receiver?: IRSForm; receiver?: IRSForm;
@ -16,7 +17,7 @@ interface SubstitutionsTabProps {
error?: ErrorData; error?: ErrorData;
substitutions: ISubstitution[]; substitutions: ISubstitution[];
setSubstitutions: React.Dispatch<React.SetStateAction<ISubstitution[]>>; setSubstitutions: React.Dispatch<React.SetStateAction<ISubstitution[] | ISynthesisSubstitution[]>>;
} }
function SubstitutionsTab({ function SubstitutionsTab({

View File

@ -0,0 +1,93 @@
'use client';
import { useEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify';
import Checkbox from '@/components/ui/Checkbox.tsx';
import FileInput from '@/components/ui/FileInput.tsx';
import Modal, { ModalProps } from '@/components/ui/Modal.tsx';
import { useRSForm } from '@/context/RSFormContext.tsx';
import { IRSForm, IRSFormUploadData, ISubstitution } from '@/models/rsform.ts';
import { EXTEOR_TRS_FILE } from '@/utils/constants.ts';
import { TabList, TabPanel, Tabs } from 'react-tabs';
import clsx from 'clsx';
import TabLabel from '@/components/ui/TabLabel.tsx';
import SubstitutionsTab from '@/dialogs/DlgInlineSynthesis/SubstitutionsTab.tsx';
import useRSFormDetails from '@/hooks/useRSFormDetails.ts';
import { LibraryItemID } from '@/models/library.ts';
import { ISynthesisData } from '@/models/synthesis.ts';
import { TabID } from '@/dialogs/DlgInlineSynthesis/DlgInlineSynthesis.tsx';
import SynthesisSubstitutionsTab from '@/pages/OssPage/SynthesisSubstitutionsTab.tsx';
import SchemaTab from '@/dialogs/DlgInlineSynthesis/SchemaTab.tsx';
import {
Node
} from '@reactflow/core';
import { useSynthesis } from '@/pages/OssPage/SynthesisContext.tsx';
interface DlgCreateSynthesisProps extends Pick<ModalProps, 'hideWindow'> {
nodeId: string;
}
export enum SynthesisTabID {
SCHEMA = 0,
SUBSTITUTIONS = 1
}
function DlgSelectInputScheme({ nodeId, hideWindow }: DlgCreateSynthesisProps) {
const controller = useSynthesis();
const [activeTab, setActiveTab] = useState(SynthesisTabID.SCHEMA);
const [selected, setSelected] = useState<LibraryItemID[]>([]);
const [substitutions, setSubstitutions] = useState<ISubstitution[]>([]);
const [donorID, setDonorID] = useState<LibraryItemID | undefined>(undefined);
const schemaPanel = useMemo(
() => (
<TabPanel>
<SchemaTab selected={donorID} setSelected={setDonorID} />
</TabPanel>
),
[donorID]
);
function handleSubmit() {
if (donorID !== undefined) {
controller.updateBounds(nodeId, donorID);
}
}
function validate() {
if (donorID === undefined) {
toast.error('Выберите источник конституент');
return false;
}
return true;
}
return (
<Modal
header="Синтез концептуальных скем"
hideWindow={hideWindow}
submitText="Привязать"
className="w-[25rem] px-6"
canSubmit={validate()}
onSubmit={handleSubmit}
>
<Tabs
selectedTabClassName="clr-selected"
className="flex flex-col"
selectedIndex={activeTab}
onSelect={setActiveTab}
>
<TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none')}>
<TabLabel label="Схема" title="Источник конституент" className="w-[8rem]" />
</TabList>
{schemaPanel}
</Tabs>
</Modal>
);
}
export default DlgSelectInputScheme;

View File

@ -0,0 +1,120 @@
'use client';
import { useMemo, useState } from 'react';
import Modal, { ModalProps } from '@/components/ui/Modal.tsx';
import { TabList, TabPanel, Tabs } from 'react-tabs';
import clsx from 'clsx';
import TabLabel from '@/components/ui/TabLabel.tsx';
import useRSFormDetails from '@/hooks/useRSFormDetails.ts';
import SynthesisSubstitutionsTab from '@/pages/OssPage/SynthesisSubstitutionsTab.tsx';
import { useSynthesis } from '@/pages/OssPage/SynthesisContext.tsx';
import { ISynthesisSubstitution } from '@/models/oss.ts';
interface DlgCreateSynthesisProps extends Pick<ModalProps, 'hideWindow'> {
nodeId: string;
onSynthesis: (data: ISynthesisSubstitution[]) => void;
}
export enum SynthesisTabID {
SCHEMA = 0,
SUBSTITUTIONS = 1
}
function DlgSynthesis({ hideWindow, nodeId, onSynthesis }: DlgCreateSynthesisProps) {
const controller = useSynthesis();
const [activeTab, setActiveTab] = useState(SynthesisTabID.SCHEMA);
const sourceLeft = useRSFormDetails({
target: controller.getNodeParentsRsform(nodeId)[0] ?
String(controller.getNodeParentsRsform(nodeId)[0]) : undefined
});
const sourceRight = useRSFormDetails({
target: controller.getNodeParentsRsform(nodeId)[1] ?
String(controller.getNodeParentsRsform(nodeId)[1]) : undefined
});
//const validated = useMemo(() => !!source.schema && selected.length > 0, [source.schema, selected]);
function handleSubmit() {
const parents = controller.getNodeParentsRsform(nodeId);
if (parents.length != 2) {
return;
}
const data: ISynthesisSubstitution[] = controller.substitutions.map((item) => ({
id: null,
operation_id: nodeId,
leftCst: item.leftCst,
rightCst: item.rightCst,
deleteRight: item.deleteRight,
takeLeftTerm: item.takeLeftTerm
}));
controller.setSubstitutions(data);
}
function validated() {
const parents = controller.getNodeParentsRsform(nodeId);
return parents.length == 2;
}
const schemaPanel = useMemo(
() => (
<TabPanel></TabPanel>
), []
);
const selectedSubstitutions = useMemo(
() => controller.getSubstitution(nodeId),
[controller, nodeId]
);
const setSelectedSubstitutions = (newElement: ISynthesisSubstitution[]) => {
controller.updateSubstitution(nodeId, newElement, controller.setSubstitutions);
};
const substitutesPanel = useMemo(
() => (
<TabPanel>
<SynthesisSubstitutionsTab
receiver={sourceLeft.schema}
source={sourceRight.schema}
substitutions={selectedSubstitutions}
setSubstitutions={setSelectedSubstitutions}
/>
</TabPanel>
),
[sourceLeft.schema, sourceRight.schema, controller]
);
return (
<Modal
header="Синтез концептуальных схем"
hideWindow={hideWindow}
submitText="Сохранить"
className="w-[25rem] px-6"
canSubmit={validated}
onSubmit={handleSubmit}
>
<Tabs
selectedTabClassName="clr-selected"
className="flex flex-col"
selectedIndex={activeTab}
onSelect={setActiveTab}
>
<TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none')}>
<TabLabel label="Схема" title="Источник конституент" className="w-[8rem]" />
<TabLabel label="Отождествления" title="Таблица отождествлений" className="w-[8rem]" />
</TabList>
{schemaPanel}
{substitutesPanel}
</Tabs>
</Modal>
);
}
export default DlgSynthesis;

View File

@ -1,113 +0,0 @@
'use client';
import {useEffect, useMemo, useState} from 'react';
import {toast} from 'react-toastify';
import Checkbox from '@/components/ui/Checkbox';
import FileInput from '@/components/ui/FileInput';
import Modal, {ModalProps} from '@/components/ui/Modal';
import {useRSForm} from '@/context/RSFormContext';
import {IRSForm, IRSFormUploadData, ISubstitution} from '@/models/rsform';
import {EXTEOR_TRS_FILE} from '@/utils/constants';
import {TabList, TabPanel, Tabs} from "react-tabs";
import clsx from "clsx";
import TabLabel from "@/components/ui/TabLabel.tsx";
import SubstitutionsTab from "@/dialogs/DlgInlineSynthesis/SubstitutionsTab.tsx";
import useRSFormDetails from "@/hooks/useRSFormDetails.ts";
import {LibraryItemID} from "@/models/library.ts";
import {ISynthesisData} from "@/models/synthesis.ts";
import {TabID} from "@/dialogs/DlgInlineSynthesis/DlgInlineSynthesis.tsx";
import SynthesisSubstitutionsTab from "@/pages/SynthesisPage/SynthesisSubstitutionsTab.tsx";
interface DlgCreateSynthesisProps extends Pick<ModalProps, 'hideWindow'> {
schemaLeftID: number;
schemaRightID: number;
onSynthesis: (data: ISynthesisData) => void;
}
export enum SynthesisTabID {
SCHEMA = 0,
SUBSTITUTIONS = 1
}
function DlgSynthesis({hideWindow, schemaLeftID, schemaRightID, onSynthesis}: DlgCreateSynthesisProps) {
const [activeTab, setActiveTab] = useState(SynthesisTabID.SCHEMA);
const sourceLeft = useRSFormDetails({target: schemaLeftID ? String(schemaLeftID) : undefined});
const sourceRight = useRSFormDetails({target: schemaRightID ? String(schemaRightID) : undefined});
const [selected, setSelected] = useState<LibraryItemID[]>([]);
const [substitutions, setSubstitutions] = useState<ISubstitution[]>([]);
//const validated = useMemo(() => !!source.schema && selected.length > 0, [source.schema, selected]);
function handleSubmit() {
if (!sourceLeft.schema || !sourceRight.schema) {
return;
}
const data: ISynthesisData = {
sourceLeft: schemaLeftID,
sourceRight: schemaRightID,
result: 1,
substitutions: substitutions.map(item => ({
original: item.deleteRight ? item.rightCst.id : item.leftCst.id,
substitution: item.deleteRight ? item.leftCst.id : item.rightCst.id,
transfer_term: !item.deleteRight && item.takeLeftTerm
}))
};
onSynthesis(data);
}
useEffect(() => {
setSelected(sourceLeft.schema && sourceRight.schema ? sourceLeft.schema?.items.map(cst => cst.id) : []);
}, [sourceLeft.schema, sourceRight.schema]);
const schemaPanel = useMemo(
() => (
<TabPanel></TabPanel>
), []
)
const substitutesPanel = useMemo(
() => (
<TabPanel>
<SynthesisSubstitutionsTab
receiver={sourceLeft.schema}
source={sourceRight.schema}
substitutions={substitutions}
setSubstitutions={setSubstitutions}
/>
</TabPanel>
),
[sourceLeft.schema, sourceRight.schema, sourceLeft.loading, sourceRight.loading, selected, substitutions]
);
return (
<Modal
header='Синтез концептуальных скем'
hideWindow={hideWindow}
submitText='Запуск'
className='w-[25rem] px-6'
//canSubmit={validated}
onSubmit={handleSubmit}
>
<Tabs
selectedTabClassName='clr-selected'
className='flex flex-col'
selectedIndex={activeTab}
onSelect={setActiveTab}
>
<TabList className={clsx('mb-3 self-center', 'flex', 'border divide-x rounded-none')}>
<TabLabel label='Схема' title='Источник конституент' className='w-[8rem]'/>
<TabLabel label='Отождествления' title='Таблица отождествлений' className='w-[8rem]'/>
</TabList>
{schemaPanel}
{substitutesPanel}
</Tabs>
</Modal>
);
}
export default DlgSynthesis;

View File

@ -6,6 +6,7 @@ import { getOssDetails } from '@/app/backendAPI';
import { type ErrorData } from '@/components/info/InfoError'; import { type ErrorData } from '@/components/info/InfoError';
import { IOperationSchema, IOperationSchemaData } from '@/models/oss'; import { IOperationSchema, IOperationSchemaData } from '@/models/oss';
import { OssLoader } from '@/models/OssLoader'; import { OssLoader } from '@/models/OssLoader';
import { AccessPolicy, LibraryItemType } from '@/models/library.ts';
function useOssDetails({ target }: { target?: string }) { function useOssDetails({ target }: { target?: string }) {
const [schema, setInner] = useState<IOperationSchema | undefined>(undefined); const [schema, setInner] = useState<IOperationSchema | undefined>(undefined);
@ -27,6 +28,24 @@ function useOssDetails({ target }: { target?: string }) {
if (!target) { if (!target) {
return; return;
} }
const staticData = {
id: Number(target),
comment: '123',
alias: 'oss1',
access_policy: AccessPolicy.PUBLIC,
editors: [],
owner: 1,
item_type: LibraryItemType.OSS,
location: '/U',
read_only: false,
subscribers: [],
time_create: '0',
time_update: '0',
title: 'TestOss',
visible: false
};
getOssDetails(target, { getOssDetails(target, {
showError: true, showError: true,
setLoading: setCustomLoading ?? setLoading, setLoading: setCustomLoading ?? setLoading,
@ -35,7 +54,11 @@ function useOssDetails({ target }: { target?: string }) {
setError(error); setError(error);
}, },
onSuccess: schema => { onSuccess: schema => {
setSchema(schema); const combinedData = {
...staticData,
...schema
}
setSchema(combinedData);
if (callback) callback(); if (callback) callback();
} }
}); });

View File

@ -11,13 +11,14 @@ import { IOperationSchema, IOperationSchemaData } from './oss';
export class OssLoader { export class OssLoader {
private schema: IOperationSchemaData; private schema: IOperationSchemaData;
constructor(input: IOperationSchemaData) { constructor(input: IOperationSchemaData) {
this.schema = input; this.schema = input;
} }
produceOSS(): IOperationSchema { produceOSS(): IOperationSchema {
const result = this.schema as IOperationSchema; const result = this.schema as IOperationSchema;
result.producedData = [1, 2, 3]; // TODO: put data processing here //result.producedData = [1, 2, 3]; // TODO: put data processing here
return result; return result;
} }
} }

View File

@ -4,12 +4,59 @@
import { ILibraryItemData } from './library'; import { ILibraryItemData } from './library';
import { UserID } from './user'; import { UserID } from './user';
import { IConstituenta, ISubstitution } from '@/models/rsform.ts';
/** /**
* Represents backend data for Schema of Synthesis Operations. * Represents backend data for Schema of Synthesis Operations.
*/ */
export interface IOperationSchemaData extends ILibraryItemData { export interface IOperationSchemaData extends ILibraryItemData {
additional_data?: number[]; input_nodes: IInputNode[];
operation_nodes: ISynthesisNode[];
edges: ISynthesisEdge[];
substitutions: ISynthesisSubstitution[];
graph: ISynthesisGraph;
}
interface ISynthesisGraph {
id: number;
status: string;
}
interface IInputNode {
id: number | null;
graph_id: number;
vertical_coordinate: number;
horizontal_coordinate: number;
rsform_id: number;
}
interface ISynthesisNode extends IInputNode {
id: number | null;
name: string;
status: string;
}
interface ISynthesisEdge {
id: number | null;
decoded_id: string;
source_handle: string;
graph_id: number;
node_from: string;
node_to: string;
}
export interface ISynthesisSubstitution extends ISubstitution {
id: number | null;
graph_id: number;
operation_id: string;
}
export interface IRunSynthesis {
operationId: number;
}
export interface IRunSynthesisResponse {
rsformId: number;
} }
/** /**
@ -19,5 +66,5 @@ export interface IOperationSchema extends IOperationSchemaData {
subscribers: UserID[]; subscribers: UserID[];
editors: UserID[]; editors: UserID[];
producedData: number[]; // TODO: modify this to store calculated state on load //producedData: number[]; // TODO: modify this to store calculated state on load
} }

View File

@ -1,9 +0,0 @@
import {LibraryItemID} from "@/models/library.ts";
import {ICstSubstitute} from "@/models/rsform.ts";
export interface ISynthesisData {
result: LibraryItemID;
sourceLeft: LibraryItemID;
sourceRight: LibraryItemID;
substitutions: ICstSubstitute[];
}

View File

@ -1,13 +1,22 @@
'use client'; 'use client';
import AnimateFade from '@/components/wrap/AnimateFade'; import AnimateFade from '@/components/wrap/AnimateFade';
import SynthesisToolbar from '@/pages/OssPage/SynthesisToolbar.tsx';
import SynthesisFlow from '@/components/ui/Synthesis/SynthesisFlow.tsx';
import { SynthesisState } from '@/pages/OssPage/SynthesisContext.tsx';
import { ReactFlowProvider } from '@reactflow/core';
function EditorOssGraph() { function EditorOssGraph() {
// TODO: Implement OSS editing UI here // TODO: Implement OSS editing UI here
return ( return (
<AnimateFade> <AnimateFade>
<div className='py-3'>Реализация графического интерфейса</div> <ReactFlowProvider>
<SynthesisState synthesisSchemaID="1">
<SynthesisToolbar />
<SynthesisFlow />
</SynthesisState>
</ReactFlowProvider>
</AnimateFade> </AnimateFade>
); );
} }

View File

@ -0,0 +1,345 @@
import { IRSFormData, ISubstitution } from '@/models/rsform.ts';
import { DataCallback, runSingleSynthesis, postSynthesisGraph } from '@/app/backendAPI.ts';
import { ISynthesisData } from '@/models/synthesis.ts';
import { createContext, Dispatch, SetStateAction, useCallback, useContext, useEffect, useState } from 'react';
import DlgSynthesis from '@/dialogs/DlgOssGraph/DlgSynthesis.tsx';
import {
Node,
Edge,
useNodesState,
useEdgesState,
type Connection,
addEdge,
getIncomers,
getOutgoers,
getConnectedEdges
} from '@reactflow/core';
import DlgSelectInputScheme from '@/dialogs/DlgOssGraph/DlgSelectInputScheme.tsx';
import { IOperationSchemaData, IRunSynthesis, ISynthesisSubstitution } from '@/models/oss.ts';
import { useOSS } from '@/context/OssContext.tsx';
import viewConstituents from '@/pages/RSFormPage/ViewConstituents';
interface ISynthesisContext {
synthesisSchemaID: string;
singleSynthesis: (nodeId: number) => void;
showSynthesis: () => void;
showSelectInput: () => void;
selectNode: (nodeId: string) => void;
getSelectedNode: () => string | undefined;
addLibrarySchema: () => void;
addSynthesisOperation: () => void;
removeItem: () => void;
runSynthesisLayer: () => void;
getNodes: () => Node[],
getEdges: () => Edge[]
setNodes: (nodes: Node[]) => void;
setEdges: (nodes: Edge[]) => void;
onNodesChange: any,
onEdgesChange: any,
onNodesDelete: any,
onConnect: any,
addBind: () => void,
updateBounds: (nodeId: string, newRsform: number) => void,
getBind: (nodeId: string) => number
getNodeParentsRsform: (nodeId: string) => number[]
saveGraph: () => void;
substitutions: ISynthesisSubstitution[]
setSubstitutions: () => void,
getSubstitution: (id: string) => ISynthesisSubstitution[],
updateSubstitution: (id: string, substitution: ISynthesisSubstitution[], setSubstitutions: React.Dispatch<React.SetStateAction<ISynthesisSubstitution[]>>) => void,
}
interface IBoundMap {
nodeId: string,
rsformId: number
}
const SynthesisContext = createContext<ISynthesisContext | null>(null);
interface SynthesisStateProps {
synthesisSchemaID: string;
children: React.ReactNode;
}
export const useSynthesis = () => {
const context = useContext(SynthesisContext);
if (context === null) {
throw new Error('useSynthesis has to be used within <SynthesisState.Provider>');
}
return context;
};
export const SynthesisState = ({ synthesisSchemaID, children }: SynthesisStateProps) => {
const [showSynthesisModal, setShowSynthesisModal] = useState(false);
const [showSelectInputModal, setShowSelectInputModal] = useState(false);
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const [bounds, setBounds] = useState<IBoundMap[]>([]);
const [substitutionBounds, setSubstitutionBounds] = useState<IBoundMap[]>([]);
const [substitutions, setSubstitutions] = useState<ISynthesisSubstitution[]>([]);
const { schema, loading, errorLoading } = useOSS();
const ossSchema = useOSS();
const getSubstitution = (operation_id: string): ISynthesisSubstitution[] => {
return substitutions.filter(substitution => substitution.operation_id === operation_id);
};
const updateSubstitution = (
operation_id: number,
newElements: ISubstitution[],
setSubstitutions: React.Dispatch<React.SetStateAction<ISynthesisSubstitution[]>>
) => {
if (!Array.isArray(newElements)) {
console.error('newElements should be an array.');
return;
}
setSubstitutions((prevSubstitutions) => {
// Обновление существующих элементов
const updatedSubstitutions = prevSubstitutions.map((substitution) => {
const newElement = newElements.find((el) => el.id === substitution.id);
if (newElement) {
// Обновляем только соответствующие элементы
return { ...substitution, ...newElement, operation_id: substitution.operation_id };
}
return substitution;
});
// Добавление новых элементов с присвоением operation_id
const newSubstitutions = newElements
.filter((newElement) => !prevSubstitutions.some((sub) => sub.id === newElement.id))
.map((newElement) => ({ ...newElement, operation_id }));
return [...updatedSubstitutions, ...newSubstitutions];
});
};
const extractEdgeId = (edgeId: string) => {
const matches = edgeId.match(/\d+/g);
const combined = matches ? matches.join('') : '';
return Number(combined);
};
function saveGraph() {
const data: IOperationSchemaData = {
graph: {
id: schema?.id,
status: 'Draft'
},
input_nodes: nodes.filter((node) => node.type == 'input').map(item =>
({
id: item.id,
vertical_coordinate: item.position.y,
horizontal_coordinate: item.position.x,
rsform_id: getBind(item.id)
})),
operation_nodes: nodes.filter((node) => node.type == 'custom').map(item =>
({
id: item.id,
vertical_coordinate: item.position.y,
horizontal_coordinate: item.position.x,
left_parent: getNodeParentsRsform(item.id)[0],
right_parent: getNodeParentsRsform(item.id)[1],
rsform_id: getBind(item.id),
name: 'name',
status: 'status'
})
),
edges: edges.map(item => ({
id: extractEdgeId(item.id),
decoded_id: item.id,
source_handle: item.sourceHandle,
node_from: item.source,
node_to: item.target
})
),
substitutions: substitutions
};
postSynthesisGraph({
data: data
});
}
useEffect(() => {
if (ossSchema.schema !== undefined) {
const initialNodes = [
...ossSchema.schema.input_nodes.map((node) => ({
id: node.id?.toString() || 'null',
data: { label: '123' },
position: { x: node.horizontal_coordinate, y: node.vertical_coordinate },
type: 'input'
})),
...ossSchema.schema.operation_nodes.map((node) => ({
id: node.id?.toString() || 'null',
data: { label: '123' },
position: { x: node.horizontal_coordinate, y: node.vertical_coordinate },
type: 'custom'
}))
];
const initialEdges = ossSchema.schema.edges.map((edge) => ({
id: edge.decoded_id,
source: String(edge.node_from),
sourceHandle: edge.source_handle,
target: String(edge.node_to)
}));
//const initialEdges = [{ id: 'reactflow__edge-2a-0', source: '2', target: '0' }, { id: 'reactflow__edge-2b-1', sourceHandle: 'b',source: '2', target: '1' }];
setNodes(initialNodes);
setEdges(initialEdges);
setSubstitutions(ossSchema.schema.substitutions);
[...ossSchema.schema.input_nodes, ...ossSchema.schema.operation_nodes].forEach((node) => {
const nodeId = node.id?.toString() || 'null';
const rsformId = node.rsform_id; // Предполагаем, что rsform_id есть в данных нод
updateBounds(nodeId, rsformId);
});
}
}, [ossSchema]);
const getBind = (nodeId: string) => {
const bound = bounds.find((item) => item.nodeId == nodeId);
return bound ? bound.rsformId : null;
};
const getBounds = (nodeId: string[]) => {
const parentBounds = bounds.filter((item) => item.nodeId in nodeId);
return parentBounds.map((item) => item.rsformId);
};
function getNodeParentsRsform(nodeId: string) {
const parentEdges = edges.filter((edge) => edge.source === nodeId);
const parentNodeIds = parentEdges.map((edge) => edge.target);
return getBounds(parentNodeIds);
}
const updateBounds = (nodeId: string, newRsform: number) => {
setBounds((prevItems) => {
const existingItem = prevItems.find((item) => item.nodeId === nodeId);
if (existingItem) {
return prevItems.map((item) =>
item.nodeId === nodeId ? { ...item, rsformId: newRsform } : item
);
} else {
return [...prevItems, { nodeId: nodeId, rsformId: newRsform }];
}
});
};
const onConnect = useCallback(
(params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)),
[setEdges]
);
const onNodeDelete = useCallback(
(nodeId: string) => {
setNodes((nodes) => nodes.filter((node) => node.id !== nodeId));
setEdges((edges) =>
edges.filter((edge) => edge.source !== nodeId && edge.target !== nodeId)
);
},
[]
);
const singleSynthesis = useCallback(
(operationId: number) => {
const data: IRunSynthesis = {
operationId: Number(operationId)
};
runSingleSynthesis({ data: data });
},
[]
);
const newLibrarySchema = {
id: String(nodes.length > 0 ? 1 + Math.max(...nodes.map(item => Number(item.id))) : 0),
type: 'input',
position: { x: 250, y: 5 },
data: {
label: 'Node 1', onDelete: onNodeDelete
}
};
const newOperation = {
id: String(nodes.length > 0 ? 1 + Math.max(...nodes.map(item => Number(item.id))) : 0),
type: 'custom',
position: { x: 350, y: 20 },
data: {
label: 'Node 1', onDelete: onNodeDelete
}
};
const selectNode = useCallback(
(nodeId: string) => {
for (const node of nodes) {
if (node.id === nodeId) {
setSelectedNode(node);
return;
}
}
}, [nodes]
);
return (
<SynthesisContext.Provider value={{
synthesisSchemaID: synthesisSchemaID,
singleSynthesis: singleSynthesis,
showSynthesis: () => setShowSynthesisModal(true),
showSelectInput: () => setShowSelectInputModal(true),
selectNode: (nodeId) => selectNode(nodeId),
getSelectedNode: () => selectedNode?.id,
addLibrarySchema: () => {
setNodes([...nodes, newLibrarySchema]);
},
addSynthesisOperation: () => {
setNodes([...nodes, newOperation]);
},
setNodes: (nodes: Node[]) => {
setNodes(nodes);
},
setEdges: (edges: Edge[]) => {
setEdges(edges);
},
getNodes: () => nodes,
getEdges: () => edges,
onNodesChange: onNodesChange,
onEdgesChange: onEdgesChange,
onConnect: onConnect,
updateBounds: updateBounds,
getNodeParentsRsform: getNodeParentsRsform,
getBind: getBind,
saveGraph: saveGraph,
setSubstitutions: setSubstitutions,
substitutions: substitutions,
updateSubstitution: updateSubstitution,
getSubstitution: getSubstitution
}}>
{showSynthesisModal ? (<DlgSynthesis
hideWindow={() => setShowSynthesisModal(false)}
nodeId={selectedNode?.id}
onSynthesis={() => singleSynthesis}
/>) : null}
{showSelectInputModal ? (<DlgSelectInputScheme
nodeId={selectedNode?.id}
hideWindow={() => setShowSelectInputModal(false)}
/>) : null}
{children}
</SynthesisContext.Provider>
);
};

View File

@ -0,0 +1,40 @@
'use client';
import { ErrorData } from '@/components/info/InfoError.tsx';
import DataLoader from '@/components/wrap/DataLoader.tsx';
import { IRSForm, ISubstitution } from '@/models/rsform.ts';
import { prefixes } from '@/utils/constants.ts';
import PickSubstitutions from '../../components/select/PickSubstitutions';
interface SynthesisSubstitutionsTabProps {
receiver?: IRSForm;
source?: IRSForm;
error?: ErrorData;
substitutions: ISubstitution[];
setSubstitutions: React.Dispatch<React.SetStateAction<ISubstitution[]>>;
}
function SynthesisSubstitutionsTab({
source,
receiver,
error,
substitutions,
setSubstitutions
}: SynthesisSubstitutionsTabProps) {
return (
<DataLoader id="dlg-substitutions-tab" className="cc-column" isLoading={false} error={error} hasNoData={!source}>
<PickSubstitutions
items={substitutions}
setItems={setSubstitutions}
rows={10}
prefixID={prefixes.cst_inline_synth_substitutes}
schema1={receiver}
schema2={source}
/>
</DataLoader>
);
}
export default SynthesisSubstitutionsTab;

View File

@ -0,0 +1,45 @@
import { useSynthesis } from '@/pages/OssPage/SynthesisContext.tsx';
import Button from '@/components/ui/Button.tsx';
import { IoMdAdd, IoMdRemove } from 'react-icons/io';
import { MdCallMerge } from 'react-icons/md';
import Overlay from '@/components/ui/Overlay.tsx';
import { MdLibraryAdd } from 'react-icons/md';
import { VscRunAll, VscSave } from 'react-icons/vsc';
function SynthesisToolbar() {
const controller = useSynthesis();
return (
<Overlay position="top-1 right-1/2 translate-x-1/2" className="flex">
<Button
title="Добавить схему из библиотеки"
icon={<MdLibraryAdd />}
onClick={() => controller.addLibrarySchema()}
/>
<Button
title="Добавить операцию слияния"
icon={<MdCallMerge />}
onClick={() => controller.addSynthesisOperation()}
/>
<Button
title={'Удалить форму'}
icon={<IoMdRemove />}
onClick={() => controller.removeItem()}
/>
<Button
icon={<VscRunAll />}
title="Синтез"
onClick={() =>{}}
/>
<Button
icon={<VscSave />}
title="Сохранить"
onClick={() => controller.saveGraph()}
/>
</Overlay>
);
}
export default SynthesisToolbar;

View File

@ -1,62 +0,0 @@
import {IRSForm, IRSFormData} from "@/models/rsform.ts";
import {DataCallback, postSynthesis} from "@/app/backendAPI.ts";
import {ISynthesisData} from "@/models/synthesis.ts";
import {createContext, useCallback, useContext, useState} from "react";
import {useLibrary} from "@/context/LibraryContext.tsx";
import {useAuth} from "@/context/AuthContext.tsx";
import useRSFormDetails from "@/hooks/useRSFormDetails.ts";
import DlgSynthesis from "@/dialogs/DlgSynthesis.tsx";
interface ISynthesisContext {
synthesisSchemaID: string;
singleSynthesis: (data: ISynthesisData, callback?: DataCallback<IRSFormData>) => void;
showSynthesis: () => void;
}
const SynthesisContext = createContext<ISynthesisContext | null>(null);
interface SynthesisStateProps {
synthesisSchemaID: string;
children: React.ReactNode;
}
export const useSynthesis = () => {
const context = useContext(SynthesisContext);
if (context === null) {
throw new Error('useSynthesis has to be used within <SynthesisState.Provider>');
}
return context;
}
export const SynthesisState = ({synthesisSchemaID, children}: SynthesisStateProps) => {
const [showSynthesisModal, setShowSynthesisModal] = useState(false)
const singleSynthesis = useCallback(
(data: ISynthesisData, callback?: DataCallback<IRSFormData>) => {
postSynthesis({
data: data,
onSuccess: newData => {
}
});
},
[]
);
return (
<SynthesisContext.Provider value={{
synthesisSchemaID: synthesisSchemaID,
singleSynthesis: singleSynthesis,
showSynthesis: () => setShowSynthesisModal(true),
}}>
{showSynthesisModal ? (< DlgSynthesis
hideWindow={() => setShowSynthesisModal(false)}
schemaLeftID={55}
schemaRightID={56}
onSynthesis={() => singleSynthesis}/>) : null}
{children}
</SynthesisContext.Provider>
)
};

View File

@ -11,31 +11,23 @@ import {BsDownload, BsUpload} from 'react-icons/bs';
import {useCallback, useState} from 'react'; import {useCallback, useState} from 'react';
//import PropTypes from "prop-types"; //import PropTypes from "prop-types";
//import {useConceptNavigation} from "@/context/NavigationContext.tsx"; //import {useConceptNavigation} from "@/context/NavigationContext.tsx";
import DlgSynthesis from "@/dialogs/DlgSynthesis.tsx"; import DlgSynthesis from "@/dialogs/DlgOssGraph/DlgSynthesis.tsx";
import SynthesisFlow from "@/components/ui/Synthesis/SynthesisFlow.tsx"; import SynthesisFlow from "@/components/ui/Synthesis/SynthesisFlow.tsx";
import {IRSFormData} from "@/models/rsform.ts"; import {IRSFormData} from "@/models/rsform.ts";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {useRSForm} from "@/context/RSFormContext.tsx"; import {useRSForm} from "@/context/RSFormContext.tsx";
import {DataCallback, postSynthesis} from "@/app/backendAPI.ts"; import {DataCallback, runSingleSynthesis} from "@/app/backendAPI.ts";
import {ISynthesisData} from "@/models/synthesis.ts"; import {ISynthesisData} from "@/models/synthesis.ts";
import useRSFormDetails from "@/hooks/useRSFormDetails.ts"; import useRSFormDetails from "@/hooks/useRSFormDetails.ts";
import {useLibrary} from "@/context/LibraryContext.tsx"; import {useLibrary} from "@/context/LibraryContext.tsx";
import {SynthesisState, useSynthesis} from "@/pages/SynthesisPage/SynthesisContext.tsx"; import {SynthesisState, useSynthesis} from "@/pages/OssPage/SynthesisContext.tsx";
import SynthesisToolbar from "@/pages/SynthesisPage/SynthesisToolbar.tsx"; import SynthesisToolbar from "@/pages/OssPage/SynthesisToolbar.tsx";
import {useParams} from "react-router-dom"; import {useParams} from "react-router-dom";
export const SynthesisPage = () => { export const SynthesisPage = () => {
const params = useParams();
//const router = useConceptNavigation(); //const router = useConceptNavigation();
//const [sampleData, setSampleData] = useState([[1, 2, 3], [4, 5, 6]]) //const [sampleData, setSampleData] = useState([[1, 2, 3], [4, 5, 6]])
return (
<SynthesisState synthesisSchemaID="1">
<SynthesisToolbar/>
<SynthesisFlow/>
</SynthesisState>
)
} }
export default SynthesisPage; export default SynthesisPage;

View File

@ -1,42 +0,0 @@
'use client';
import { ErrorData } from '@/components/info/InfoError';
import DataLoader from '@/components/wrap/DataLoader';
import { ConstituentaID, IRSForm, ISubstitution } from '@/models/rsform';
import { prefixes } from '@/utils/constants';
import SubstitutionsPicker from '../../components/select/SubstitutionsPicker';
interface SynthesisSubstitutionsTabProps {
receiver?: IRSForm;
source?: IRSForm;
error?: ErrorData;
substitutions: ISubstitution[];
setSubstitutions: React.Dispatch<React.SetStateAction<ISubstitution[]>>;
}
function SynthesisSubstitutionsTab({
source,
receiver,
error,
substitutions,
setSubstitutions
}: SynthesisSubstitutionsTabProps) {
return (
<DataLoader id='dlg-substitutions-tab' className='cc-column' isLoading={false} error={error} hasNoData={!source}>
<SubstitutionsPicker
items={substitutions}
setItems={setSubstitutions}
rows={10}
prefixID={prefixes.cst_inline_synth_substitutes}
schema1={receiver}
schema2={source}
/>
</DataLoader>
);
}
export default SynthesisSubstitutionsTab;

View File

@ -1,39 +0,0 @@
import {useSynthesis} from "@/pages/SynthesisPage/SynthesisContext.tsx";
import Button from "@/components/ui/Button.tsx";
import {IoMdAdd, IoMdRemove} from "react-icons/io";
import {MdCallMerge} from "react-icons/md";
import {BsDownload, BsUpload} from "react-icons/bs";
import Overlay from "@/components/ui/Overlay.tsx";
function SynthesisToolbar() {
const controller = useSynthesis()
return (
<Overlay position='top-1 right-1/2 translate-x-1/2' className='flex'>
<Button
title='Добавить форму'
icon={<IoMdAdd/>}
/>
<Button
title={'Удалить форму'}
icon={<IoMdRemove/>}
/>
<Button
icon={<MdCallMerge/>}
title='Синтез'
onClick={() => controller.showSynthesis()}
/>
<Button
icon={<BsDownload/>}
title='Импорт формы'
/>
<Button
title='Экспорт формы'
icon={<BsUpload/>}
/>
</Overlay>
)
}
export default SynthesisToolbar;