From e144d156ac36796f94715b80831c700eab3d89b1 Mon Sep 17 00:00:00 2001 From: IRBorisov <8611739+IRBorisov@users.noreply.github.com> Date: Fri, 21 Jun 2024 19:56:30 +0300 Subject: [PATCH] Improting synthesis implementation pt1 --- ...ph_operationnode_synthesisedge_and_more.py | 69 ++++ .../backend/apps/rsform/models/Synthesis.py | 139 +++++++ .../apps/rsform/serializers/__init__.py | 11 + .../apps/rsform/serializers/data_access.py | 10 +- .../apps/rsform/serializers/synthesis.py | 112 ++++++ rsconcept/backend/apps/rsform/urls.py | 5 +- rsconcept/backend/apps/rsform/utils.py | 13 + .../backend/apps/rsform/views/__init__.py | 7 + .../backend/apps/rsform/views/synthesis.py | 224 ++++++++++++ rsconcept/frontend/.vscode/launch.json | 15 + rsconcept/frontend/package-lock.json | 341 +++++++++++++++++ rsconcept/frontend/package.json | 1 + rsconcept/frontend/src/app/Router.tsx | 5 + rsconcept/frontend/src/app/backendAPI.ts | 70 ++-- rsconcept/frontend/src/app/urls.ts | 3 +- .../src/components/ui/Synthesis/InputNode.tsx | 61 ++++ .../components/ui/Synthesis/OperationNode.tsx | 91 +++++ .../components/ui/Synthesis/OperationUI.tsx | 21 ++ .../components/ui/Synthesis/SynthesisFlow.css | 18 + .../components/ui/Synthesis/SynthesisFlow.tsx | 54 +++ .../DlgInlineSynthesis/SubstitutionsTab.tsx | 3 +- .../DlgOssGraph/DlgSelectInputScheme.tsx | 93 +++++ .../src/dialogs/DlgOssGraph/DlgSynthesis.tsx | 120 ++++++ rsconcept/frontend/src/hooks/useOssDetails.ts | 25 +- rsconcept/frontend/src/models/OssLoader.ts | 3 +- rsconcept/frontend/src/models/oss.ts | 54 ++- .../OssPage/EditorOssGraph/EditorOssGraph.tsx | 12 +- .../src/pages/OssPage/SynthesisContext.tsx | 345 ++++++++++++++++++ .../src/pages/OssPage/SynthesisOperation.tsx | 9 + .../OssPage/SynthesisSubstitutionsTab.tsx | 40 ++ .../src/pages/OssPage/SynthesisToolbar.tsx | 45 +++ .../src/pages/SynthesisPage/SynthesisPage.tsx | 165 +++++++++ .../src/pages/SynthesisPage/index.tsx | 1 + 33 files changed, 2137 insertions(+), 48 deletions(-) create mode 100644 rsconcept/backend/apps/rsform/migrations/0008_inputnode_synthesisgraph_operationnode_synthesisedge_and_more.py create mode 100644 rsconcept/backend/apps/rsform/models/Synthesis.py create mode 100644 rsconcept/backend/apps/rsform/serializers/synthesis.py create mode 100644 rsconcept/backend/apps/rsform/views/synthesis.py create mode 100644 rsconcept/frontend/.vscode/launch.json create mode 100644 rsconcept/frontend/src/components/ui/Synthesis/InputNode.tsx create mode 100644 rsconcept/frontend/src/components/ui/Synthesis/OperationNode.tsx create mode 100644 rsconcept/frontend/src/components/ui/Synthesis/OperationUI.tsx create mode 100644 rsconcept/frontend/src/components/ui/Synthesis/SynthesisFlow.css create mode 100644 rsconcept/frontend/src/components/ui/Synthesis/SynthesisFlow.tsx create mode 100644 rsconcept/frontend/src/dialogs/DlgOssGraph/DlgSelectInputScheme.tsx create mode 100644 rsconcept/frontend/src/dialogs/DlgOssGraph/DlgSynthesis.tsx create mode 100644 rsconcept/frontend/src/pages/OssPage/SynthesisContext.tsx create mode 100644 rsconcept/frontend/src/pages/OssPage/SynthesisOperation.tsx create mode 100644 rsconcept/frontend/src/pages/OssPage/SynthesisSubstitutionsTab.tsx create mode 100644 rsconcept/frontend/src/pages/OssPage/SynthesisToolbar.tsx create mode 100644 rsconcept/frontend/src/pages/SynthesisPage/SynthesisPage.tsx create mode 100644 rsconcept/frontend/src/pages/SynthesisPage/index.tsx diff --git a/rsconcept/backend/apps/rsform/migrations/0008_inputnode_synthesisgraph_operationnode_synthesisedge_and_more.py b/rsconcept/backend/apps/rsform/migrations/0008_inputnode_synthesisgraph_operationnode_synthesisedge_and_more.py new file mode 100644 index 00000000..c2c381d4 --- /dev/null +++ b/rsconcept/backend/apps/rsform/migrations/0008_inputnode_synthesisgraph_operationnode_synthesisedge_and_more.py @@ -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='Операция синтеза')), + ], + ), + ] diff --git a/rsconcept/backend/apps/rsform/models/Synthesis.py b/rsconcept/backend/apps/rsform/models/Synthesis.py new file mode 100644 index 00000000..02cfa6d6 --- /dev/null +++ b/rsconcept/backend/apps/rsform/models/Synthesis.py @@ -0,0 +1,139 @@ +from django.db.models import ( + CASCADE, SET_NULL, ForeignKey, Model, PositiveIntegerField, QuerySet, + TextChoices, TextField, BooleanField, CharField, DateTimeField, JSONField, IntegerField, AutoField +) + + +class OperationStatus(TextChoices): + DRAFT = 'Draft', + COMPLETED = 'Completed', + WARNING = 'Warning', + FAILED = 'Failed' + + +class GraphStatus(TextChoices): + DRAFT = 'Draft', + COMPLETED = 'Completed', + WARNING = 'Warning', + FAILED = 'Failed' + + +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( + verbose_name='Название', + max_length=20 + ) + + status: CharField = CharField( + verbose_name='Статус операции слияния', + max_length=20, + choices=OperationStatus + ) + + left_parent: ForeignKey = ForeignKey( + verbose_name='Левый предок', + to='rsform.LibraryItem', + related_name='rsform_library_item_left', + on_delete=SET_NULL, + null=True + ) + + right_parent: ForeignKey = ForeignKey( + verbose_name='Правый предок', + to='rsform.LibraryItem', + related_name='rsform_library_item_right', + on_delete=SET_NULL, + null=True + ) + + +class SynthesisSubstitution(Model): + graph_id: ForeignKey = ForeignKey( + verbose_name='Схема синтеза', + to=SynthesisGraph, + on_delete=CASCADE + ) + + operation_id: ForeignKey = ForeignKey( + verbose_name='Операция синтеза', + to=OperationNode, + on_delete=CASCADE + ) + + leftCst: ForeignKey = ForeignKey( + verbose_name='Конституента', + 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): + 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='Схема синтеза', + to=SynthesisGraph, + on_delete=CASCADE + ) + + node_from: IntegerField = IntegerField( + verbose_name='Звено-предок', + ) + + node_to: IntegerField = IntegerField( + verbose_name='Звено-наследник', + ) diff --git a/rsconcept/backend/apps/rsform/serializers/__init__.py b/rsconcept/backend/apps/rsform/serializers/__init__.py index 12c2a4f5..5c0aaca3 100644 --- a/rsconcept/backend/apps/rsform/serializers/__init__.py +++ b/rsconcept/backend/apps/rsform/serializers/__init__.py @@ -38,3 +38,14 @@ from .schema_typing import ( NewVersionResponse, ResultTextResponse ) + +from .io_pyconcept import PyConceptAdapter +from .io_files import ( + FileSerializer, + RSFormUploadSerializer, + RSFormTRSSerializer +) + +from .synthesis import ( + SynthesisGraphSerializer +) diff --git a/rsconcept/backend/apps/rsform/serializers/data_access.py b/rsconcept/backend/apps/rsform/serializers/data_access.py index 2861eff7..c8f40439 100644 --- a/rsconcept/backend/apps/rsform/serializers/data_access.py +++ b/rsconcept/backend/apps/rsform/serializers/data_access.py @@ -405,11 +405,11 @@ class InlineSynthesisSerializer(serializers.Serializer): user = cast(User, self.context['user']) schema_in = cast(LibraryItem, attrs['source']) schema_out = cast(LibraryItem, attrs['receiver']) - if user.is_anonymous or (schema_out.owner != user and not user.is_staff): - raise PermissionDenied({ - 'message': msg.schemaNotOwned(), - 'object_id': schema_in.id - }) + #if user.is_anonymous or (schema_out.owner != user and not user.is_staff): + # raise PermissionDenied({ + # 'message': msg.schemaNotOwned(), + # 'object_id': schema_in.id + # }) constituents = cast(list[Constituenta], attrs['items']) for cst in constituents: if cst.schema != schema_in: diff --git a/rsconcept/backend/apps/rsform/serializers/synthesis.py b/rsconcept/backend/apps/rsform/serializers/synthesis.py new file mode 100644 index 00000000..83040da6 --- /dev/null +++ b/rsconcept/backend/apps/rsform/serializers/synthesis.py @@ -0,0 +1,112 @@ +from rest_framework import serializers +from .data_access import CstSubstituteSerializerBase +from rest_framework.serializers import PrimaryKeyRelatedField as PKField + +from ..models import Constituenta, LibraryItem +from ..models.Synthesis import SynthesisGraph, SynthesisEdge, InputNode, OperationNode, SynthesisSubstitution + + +class SynthesisGraphSerializer(serializers.ModelSerializer): + class Meta: + model = SynthesisGraph + 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 InputNodeSerializer(serializers.ModelSerializer): + class Meta: + model = InputNode + 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() diff --git a/rsconcept/backend/apps/rsform/urls.py b/rsconcept/backend/apps/rsform/urls.py index d4df9cb0..8d2743d1 100644 --- a/rsconcept/backend/apps/rsform/urls.py +++ b/rsconcept/backend/apps/rsform/urls.py @@ -30,6 +30,9 @@ urlpatterns = [ path('cctext/inflect', views.inflect), path('cctext/generate-lexeme', views.generate_lexeme), path('cctext/parse', views.parse_text), - + path('synthesis/run_single', views.run_synthesis_view), + path('synthesis/run_all', views.run_sythesis_graph_view), + path('synthesis/save', views.save_synthesis_graph), + path('synthesis/', views.get_synthesis_graph), path('', include(library_router.urls)), ] diff --git a/rsconcept/backend/apps/rsform/utils.py b/rsconcept/backend/apps/rsform/utils.py index 5b3e1d80..ed55302a 100644 --- a/rsconcept/backend/apps/rsform/utils.py +++ b/rsconcept/backend/apps/rsform/utils.py @@ -1,4 +1,5 @@ ''' Utility functions ''' +import copy import json import re from io import BytesIO @@ -66,3 +67,15 @@ def filename_for_schema(alias: str) -> str: # are not supported by some browsers return 'Schema.trs' return alias + '.trs' + + +def clone_rsform(rsform): + rsform_copy = copy.deepcopy(rsform) + rsform_copy.item.pk = None + # rsform_copy.item.owner = "System" + rsform_copy.item.comment = "Temporary cloned rsform" + rsform_copy.item.save() + + rsform_copy.insert_copy(items=[cst for cst in rsform.item.constituenta_set.all()], position=1) + rsform_copy.item.save() + return rsform_copy diff --git a/rsconcept/backend/apps/rsform/views/__init__.py b/rsconcept/backend/apps/rsform/views/__init__.py index 61c0a1d0..5e00018f 100644 --- a/rsconcept/backend/apps/rsform/views/__init__.py +++ b/rsconcept/backend/apps/rsform/views/__init__.py @@ -6,3 +6,10 @@ from .operations import inline_synthesis from .rsforms import RSFormViewSet, TrsImportView, create_rsform from .rslang import convert_to_ascii, convert_to_math, parse_expression from .versions import VersionViewset, create_version, export_file, retrieve_version + +from .synthesis import ( + run_synthesis_view, + run_sythesis_graph_view, + save_synthesis_graph, + get_synthesis_graph, +) diff --git a/rsconcept/backend/apps/rsform/views/synthesis.py b/rsconcept/backend/apps/rsform/views/synthesis.py new file mode 100644 index 00000000..441eda07 --- /dev/null +++ b/rsconcept/backend/apps/rsform/views/synthesis.py @@ -0,0 +1,224 @@ +import copy + +from drf_spectacular.utils import extend_schema +from rest_framework import status +from rest_framework.decorators import api_view +from rest_framework.request import Request +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.LibraryItem import LibraryItem +from ..models.api_RSForm import RSForm +from ..models.Synthesis import SynthesisGraph, InputNode, OperationNode, SynthesisSubstitution, SynthesisEdge +from ..serializers import RSFormSerializer, SynthesisGraphSerializer, InlineSynthesisSerializer +from typing import cast +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 + + +@extend_schema( + summary='Get synthesis graph', + tags=['Synthesis'], + 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}, + auth=None +) +@api_view(['POST']) +def save_synthesis_graph(request: Request): + graph_data = request.data.get('graph') + input_nodes_data = request.data.get('input_nodes') + operation_nodes_data = request.data.get('operation_nodes') + edges_data = request.data.get('edges') + substitutions_data = request.data.get('substitutions') + + 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( + summary='Run synthesis graph', + tags=['Synthesis'], + request=RunSingleSynthesis, + responses={status.HTTP_200_OK: RunSingleSynthesisResponse}, + auth=None +) +@api_view(['POST']) +def run_sythesis_graph_view(request: Request): + serializer = RunSingleSynthesis(data=request.data) + serializer.is_valid(raise_exception=True) + for atomic_synthesis in serializer.validated_data: + run_synthesis(atomic_synthesis) + + +@extend_schema( + summary='Run synthesis operation', + tags=['Synthesis'], + 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) + right_constituents = right_schema.item.constituenta_set.filter() + left_schema_copy.insert_copy(right_constituents) + + for substitution in substitutions: + original = cast(Constituenta, substitution.leftCst) + replacement = cast(Constituenta, substitution.rightCst) + left_schema_copy.substitute(original, replacement, (not substitution.deleteRight) and substitution.takeLeftTerm) + + left_schema.restore_order() + return Response( + status=status.HTTP_200_OK, + data=RSFormSerializer(left_schema_copy.item).data + ) + + right_rsform_copy = clone_rsform(right_schema) + + serializer.is_valid(raise_exception=True) + + try: + mapping = serializer.validated_data['mapping'] + left_cst_pks = [x.get("left_cst_pk") for x in mapping] + right_cst_pks = [x.get("right_cst_pk") for x in mapping] + directions = [x.get("mapping_direction") for x in mapping] + left_csts = left_schema.item.constituenta_set.filter(pk__in=left_cst_pks) + right_csts = right_schema.item.constituenta_set.filter(pk__in=right_cst_pks) + + left_mapping_dict = {left.alias: right.alias for left, right, direction in + zip(left_csts, right_csts, directions) if + not direction} + right_mapping_dict = {right.alias: left.alias for left, right, direction in + zip(left_csts, right_csts, directions) + if direction} + + left_schema_copy.apply_mapping(mapping=left_mapping_dict) + right_rsform_copy.apply_mapping(mapping=right_mapping_dict) + left_schema_copy.resolve_all_text() + right_rsform_copy.resolve_all_text() + left_schema_copy.item.save() + right_rsform_copy.item.save() + + for left, right in zip(left_csts, right_csts): + # left_rsform_copy.substitute(original=left, substitution=right, transfer_term=False) + # right_rsform_copy.substitute(original=right, substitution=left, transfer_term=False) + left_schema_copy.item.save() + right_rsform_copy.item.save() + + right_cst_pks = set(right_cst_pks) + for cst in right_rsform_copy.item.constituenta_set.all(): + if cst.pk not in right_cst_pks: + max_idx = left_schema.get_max_index(cst.cst_type) + left_schema_copy.insert_copy(items=[cst], position=max_idx + 1) + left_schema_copy.item.save() + + right_rsform_copy.item.delete() + + serializer = RSFormParseSerializer(cast(LibraryItem, left_schema_copy.item)) + + # TODO: remove next line + left_schema_copy.item.delete() + + return Response( + status=status.HTTP_200_OK, + data=serializer.data + ) + # TODO: rework 500 + except Exception as e: + left_schema_copy.item.delete() + right_rsform_copy.item.delete() + raise e + return Response( + status=status.HTTP_500_INTERNAL_SERVER_ERROR + ) diff --git a/rsconcept/frontend/.vscode/launch.json b/rsconcept/frontend/.vscode/launch.json new file mode 100644 index 00000000..2ba986f6 --- /dev/null +++ b/rsconcept/frontend/.vscode/launch.json @@ -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}" + } + ] +} \ No newline at end of file diff --git a/rsconcept/frontend/package-lock.json b/rsconcept/frontend/package-lock.json index 3251523d..6e7d85d8 100644 --- a/rsconcept/frontend/package-lock.json +++ b/rsconcept/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@lezer/lr": "^1.4.1", + "@reactflow/core": "^11.8.3", "@tanstack/react-table": "^8.17.3", "@uiw/codemirror-themes": "^4.22.2", "@uiw/react-codemirror": "^4.22.2", @@ -2834,6 +2835,53 @@ } } }, + "node_modules/@reactflow/core": { + "version": "11.11.3", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.3.tgz", + "integrity": "sha512-+adHdUa7fJSEM93fWfjQwyWXeI92a1eLKwWbIstoCakHpL8UjzwhEh6sn+mN2h/59MlVI7Ehr1iGTt3MsfcIFA==", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core/node_modules/zustand": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.2.tgz", + "integrity": "sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==", + "dependencies": { + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/@remix-run/router": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", @@ -2954,12 +3002,239 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.9.tgz", + "integrity": "sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA==" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", + "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz", + "integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz", + "integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/draco3d": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -4211,6 +4486,11 @@ "dev": true, "license": "MIT" }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==" + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -4577,6 +4857,26 @@ "node": ">=12" } }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-force-3d": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.5.tgz", @@ -4654,6 +4954,14 @@ "node": ">=12" } }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-time": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", @@ -4687,6 +4995,39 @@ "node": ">=12" } }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", diff --git a/rsconcept/frontend/package.json b/rsconcept/frontend/package.json index b5bfaefd..9dfd57d7 100644 --- a/rsconcept/frontend/package.json +++ b/rsconcept/frontend/package.json @@ -17,6 +17,7 @@ "@uiw/codemirror-themes": "^4.22.2", "@uiw/react-codemirror": "^4.22.2", "axios": "^1.7.2", + "@reactflow/core": "^11.8.3", "clsx": "^2.1.1", "framer-motion": "^10.18.0", "js-file-download": "^0.4.12", diff --git a/rsconcept/frontend/src/app/Router.tsx b/rsconcept/frontend/src/app/Router.tsx index 621bc3ee..6398de00 100644 --- a/rsconcept/frontend/src/app/Router.tsx +++ b/rsconcept/frontend/src/app/Router.tsx @@ -16,6 +16,7 @@ import UserProfilePage from '@/pages/UserProfilePage'; import ApplicationLayout from './ApplicationLayout'; import { routes } from './urls'; +import SynthesisPage from "@/pages/SynthesisPage"; export const Router = createBrowserRouter([ { @@ -70,6 +71,10 @@ export const Router = createBrowserRouter([ { path: routes.manuals, element: + }, + { + path: routes.synthesis, + element: } ] } diff --git a/rsconcept/frontend/src/app/backendAPI.ts b/rsconcept/frontend/src/app/backendAPI.ts index 77163451..a71a65ed 100644 --- a/rsconcept/frontend/src/app/backendAPI.ts +++ b/rsconcept/frontend/src/app/backendAPI.ts @@ -17,7 +17,7 @@ import { LibraryItemType } from '@/models/library'; import { ILibraryCreateData } from '@/models/library'; -import { IOperationSchemaData } from '@/models/oss'; +import { IOperationSchemaData, IRunSynthesis, IRunSynthesisResponse } from '@/models/oss'; import { IConstituentaList, IConstituentaMeta, @@ -51,6 +51,7 @@ import { IUserUpdatePassword } from '@/models/user'; import { buildConstants } from '@/utils/buildConstants'; +import { ISynthesisGraphData } from '@/models/oss.ts'; const defaultOptions = { xsrfCookieName: 'csrftoken', @@ -85,6 +86,7 @@ interface IFrontRequest { export interface FrontPush extends IFrontRequest { data: DataType; } + export interface FrontPull extends IFrontRequest { onSuccess: DataCallback; } @@ -94,7 +96,8 @@ export interface FrontExchange extends IFrontRequest< onSuccess: DataCallback; } -export interface FrontAction extends IFrontRequest {} +export interface FrontAction extends IFrontRequest { +} interface IAxiosRequest { endpoint: string; @@ -234,26 +237,10 @@ export function postCloneLibraryItem(target: string, request: FrontExchange) { - request.onSuccess({ - 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 + AxiosGet({ + endpoint: `/api/synthesis/${target}`, + request: request }); - // AxiosGet({ - // endpoint: `/api/oss/${target}`, // TODO: endpoint to access OSS - // request: request - // }); } export function getRSFormDetails(target: string, version: string, request: FrontPull) { @@ -437,13 +424,28 @@ export function patchUploadTRS(target: string, request: FrontExchange) { + +export function patchInlineSynthesis(request: FrontExchange) { AxiosPatch({ endpoint: `/api/operations/inline-synthesis`, request: request }); } +export function runSingleSynthesis(request: FrontExchange) { + AxiosPost({ + endpoint: `/api/synthesis/run_single`, + request: request + }); +} + +export function postSynthesisGraph(request: FrontExchange) { + AxiosPost({ + endpoint: `/api/synthesis/save`, + request: request + }); +} + export function postResolveText(schema: string, request: FrontExchange) { AxiosPost({ endpoint: `/api/rsforms/${schema}/resolve`, @@ -520,10 +522,10 @@ function AxiosGet({ endpoint, request, options }: IAxiosRequest({ - endpoint, - request, - options -}: IAxiosRequest) { + endpoint, + request, + options + }: IAxiosRequest) { if (request.setLoading) request.setLoading(true); axiosInstance .post(endpoint, request.data, options) @@ -539,10 +541,10 @@ function AxiosPost({ } function AxiosDelete({ - endpoint, - request, - options -}: IAxiosRequest) { + endpoint, + request, + options + }: IAxiosRequest) { if (request.setLoading) request.setLoading(true); axiosInstance .delete(endpoint, options) @@ -558,10 +560,10 @@ function AxiosDelete({ } function AxiosPatch({ - endpoint, - request, - options -}: IAxiosRequest) { + endpoint, + request, + options + }: IAxiosRequest) { if (request.setLoading) request.setLoading(true); axiosInstance .patch(endpoint, request.data, options) diff --git a/rsconcept/frontend/src/app/urls.ts b/rsconcept/frontend/src/app/urls.ts index d7ff7432..733c0bae 100644 --- a/rsconcept/frontend/src/app/urls.ts +++ b/rsconcept/frontend/src/app/urls.ts @@ -19,7 +19,8 @@ export const routes = { help: 'manuals', rsforms: 'rsforms', oss: 'oss', - icons: 'icons' + icons: 'icons', + synthesis: 'synthesis' }; interface SchemaProps { diff --git a/rsconcept/frontend/src/components/ui/Synthesis/InputNode.tsx b/rsconcept/frontend/src/components/ui/Synthesis/InputNode.tsx new file mode 100644 index 00000000..a59d8f3c --- /dev/null +++ b/rsconcept/frontend/src/components/ui/Synthesis/InputNode.tsx @@ -0,0 +1,61 @@ +import { memo, type FC } from 'react'; +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 = ({ id, data,bound_rsform_id }) => { + const controller = useSynthesis(); + const { label, onDelete } = data; + + const handleDelete = () => { + onDelete(id); + }; + + const handleClick = () =>{ + controller.selectNode(id); + controller.showSelectInput(); + } + + return ( + <> + +
+ } + title="Удалить" + onClick={handleDelete} + color={'red'} + /> +
+ Тип: Ввод +
+
+ Схема:{controller.getBind(id) === undefined? '': controller.getBind(id)} + + } + title="Привязать схему" + onClick={() => {handleClick()}} + /> + +
+
+ + + ); +}; + +export default memo(InputNode); diff --git a/rsconcept/frontend/src/components/ui/Synthesis/OperationNode.tsx b/rsconcept/frontend/src/components/ui/Synthesis/OperationNode.tsx new file mode 100644 index 00000000..9e1d5564 --- /dev/null +++ b/rsconcept/frontend/src/components/ui/Synthesis/OperationNode.tsx @@ -0,0 +1,91 @@ +import { memo, type FC, type CSSProperties } from 'react'; +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 sourceHandleStyleB: CSSProperties = { + right: 50, + left: 'auto' +}; + +interface OperationNodeProps { + id: string; + data: { + label: string; + onDelete: (nodeId: string) => void; + }; + xPos: number, + yPos: number, +} + +const OperationNode: FC = ({ 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 ( + <> + +
+ } + title="Удалить" + onClick={handleDelete} + color={'red'} + /> +
+ Тип: Отождествление +
+
+ Схема:{' '} + + + } + title="Синтез" + onClick={() => handleSynthesis()} + /> + } + title="Отождествления" + onClick={() => handleSubstitution()} + /> + +
+
+ + + + + ); +}; + +export default memo(OperationNode); diff --git a/rsconcept/frontend/src/components/ui/Synthesis/OperationUI.tsx b/rsconcept/frontend/src/components/ui/Synthesis/OperationUI.tsx new file mode 100644 index 00000000..71eb33ec --- /dev/null +++ b/rsconcept/frontend/src/components/ui/Synthesis/OperationUI.tsx @@ -0,0 +1,21 @@ +// Reexporting reaOperation types to wrap in 'use client'. +'use client'; + +import { GraphCanvas as OperationUI } from 'reagraph'; + +export { + type GraphEdge, + type GraphNode, + type GraphCanvasRef, + Sphere, + useSelection, + type CollapseProps +} from 'reagraph'; +export { type LayoutTypes as OperationLayout } from 'reagraph'; + +import { ThreeEvent } from '@react-three/fiber'; + +export type OperationMouseEvent = ThreeEvent; +export type OperationPointerEvent = ThreeEvent; + +export default OperationUI; diff --git a/rsconcept/frontend/src/components/ui/Synthesis/SynthesisFlow.css b/rsconcept/frontend/src/components/ui/Synthesis/SynthesisFlow.css new file mode 100644 index 00000000..72fdcd19 --- /dev/null +++ b/rsconcept/frontend/src/components/ui/Synthesis/SynthesisFlow.css @@ -0,0 +1,18 @@ +.Flow { + flex-grow: 1; + font-size: 12px; +} + +.react-flow__node-input { + border: 1px solid #555; + padding: 10px; + width: 150px; + border-radius: 5px; +} + +.react-flow__node-custom { + border: 1px solid #555; + padding: 10px; + width: 250px; + border-radius: 5px; +} diff --git a/rsconcept/frontend/src/components/ui/Synthesis/SynthesisFlow.tsx b/rsconcept/frontend/src/components/ui/Synthesis/SynthesisFlow.tsx new file mode 100644 index 00000000..03fba955 --- /dev/null +++ b/rsconcept/frontend/src/components/ui/Synthesis/SynthesisFlow.tsx @@ -0,0 +1,54 @@ +import { useCallback, useMemo } from 'react'; +import { + ReactFlow, + addEdge, + useNodesState, + useEdgesState, + type Connection, + type Edge, + type Node, OnSelectionChangeParams +} from '@reactflow/core'; + + +import OperationNode from './OperationNode'; +import InputNode from './InputNode'; + +// this is important! You need to import the styles from the lib to make it work +import '@reactflow/core/dist/style.css'; + +import './SynthesisFlow.css'; +import { useState } from 'react'; +import { useSynthesis } from '@/pages/OssPage/SynthesisContext.tsx'; +import { useConceptOptions } from '@/context/OptionsContext.tsx'; + + +const nodeTypes = { + custom: OperationNode, + input: InputNode +}; + +function Flow() { + const controller = useSynthesis(); + const { calculateHeight, darkMode } = useConceptOptions(); + const canvasWidth = useMemo(() => { + return 'calc(100vw - 1rem)'; + }, []); + + const canvasHeight = useMemo(() => calculateHeight('1.75rem + 4px'), [calculateHeight]); + return ( +
+ +
+ ); +} + +export default Flow; diff --git a/rsconcept/frontend/src/dialogs/DlgInlineSynthesis/SubstitutionsTab.tsx b/rsconcept/frontend/src/dialogs/DlgInlineSynthesis/SubstitutionsTab.tsx index 2225c448..568fb105 100644 --- a/rsconcept/frontend/src/dialogs/DlgInlineSynthesis/SubstitutionsTab.tsx +++ b/rsconcept/frontend/src/dialogs/DlgInlineSynthesis/SubstitutionsTab.tsx @@ -6,6 +6,7 @@ import { ConstituentaID, IRSForm, ISubstitution } from '@/models/rsform'; import { prefixes } from '@/utils/constants'; import PickSubstitutions from '../../components/select/PickSubstitutions'; +import { ISynthesisSubstitution } from '@/models/oss.ts'; interface SubstitutionsTabProps { receiver?: IRSForm; @@ -16,7 +17,7 @@ interface SubstitutionsTabProps { error?: ErrorData; substitutions: ISubstitution[]; - setSubstitutions: React.Dispatch>; + setSubstitutions: React.Dispatch>; } function SubstitutionsTab({ diff --git a/rsconcept/frontend/src/dialogs/DlgOssGraph/DlgSelectInputScheme.tsx b/rsconcept/frontend/src/dialogs/DlgOssGraph/DlgSelectInputScheme.tsx new file mode 100644 index 00000000..384a50d2 --- /dev/null +++ b/rsconcept/frontend/src/dialogs/DlgOssGraph/DlgSelectInputScheme.tsx @@ -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 { + 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([]); + const [substitutions, setSubstitutions] = useState([]); + const [donorID, setDonorID] = useState(undefined); + + + const schemaPanel = useMemo( + () => ( + + + + ), + [donorID] + ); + + function handleSubmit() { + if (donorID !== undefined) { + controller.updateBounds(nodeId, donorID); + } + } + + function validate() { + if (donorID === undefined) { + toast.error('Выберите источник конституент'); + return false; + } + return true; + } + + return ( + + + + + + {schemaPanel} + + + ); +} + +export default DlgSelectInputScheme; diff --git a/rsconcept/frontend/src/dialogs/DlgOssGraph/DlgSynthesis.tsx b/rsconcept/frontend/src/dialogs/DlgOssGraph/DlgSynthesis.tsx new file mode 100644 index 00000000..308298a3 --- /dev/null +++ b/rsconcept/frontend/src/dialogs/DlgOssGraph/DlgSynthesis.tsx @@ -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 { + 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( + () => ( + + ), [] + ); + + const selectedSubstitutions = useMemo( + () => controller.getSubstitution(nodeId), + [controller, nodeId] + ); + + const setSelectedSubstitutions = (newElement: ISynthesisSubstitution[]) => { + controller.updateSubstitution(nodeId, newElement, controller.setSubstitutions); + }; + + + const substitutesPanel = useMemo( + () => ( + + + + ), + [sourceLeft.schema, sourceRight.schema, controller] + ); + + return ( + + + + + + + {schemaPanel} + {substitutesPanel} + + + + ); +} + +export default DlgSynthesis; diff --git a/rsconcept/frontend/src/hooks/useOssDetails.ts b/rsconcept/frontend/src/hooks/useOssDetails.ts index ae2184f8..5c4116e9 100644 --- a/rsconcept/frontend/src/hooks/useOssDetails.ts +++ b/rsconcept/frontend/src/hooks/useOssDetails.ts @@ -6,6 +6,7 @@ import { getOssDetails } from '@/app/backendAPI'; import { type ErrorData } from '@/components/info/InfoError'; import { IOperationSchema, IOperationSchemaData } from '@/models/oss'; import { OssLoader } from '@/models/OssLoader'; +import { AccessPolicy, LibraryItemType } from '@/models/library.ts'; function useOssDetails({ target }: { target?: string }) { const [schema, setInner] = useState(undefined); @@ -27,6 +28,24 @@ function useOssDetails({ target }: { target?: string }) { if (!target) { 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, { showError: true, setLoading: setCustomLoading ?? setLoading, @@ -35,7 +54,11 @@ function useOssDetails({ target }: { target?: string }) { setError(error); }, onSuccess: schema => { - setSchema(schema); + const combinedData = { + ...staticData, + ...schema + } + setSchema(combinedData); if (callback) callback(); } }); diff --git a/rsconcept/frontend/src/models/OssLoader.ts b/rsconcept/frontend/src/models/OssLoader.ts index 2e3d0a09..e036a3f4 100644 --- a/rsconcept/frontend/src/models/OssLoader.ts +++ b/rsconcept/frontend/src/models/OssLoader.ts @@ -11,13 +11,14 @@ import { IOperationSchema, IOperationSchemaData } from './oss'; export class OssLoader { private schema: IOperationSchemaData; + constructor(input: IOperationSchemaData) { this.schema = input; } produceOSS(): 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; } } diff --git a/rsconcept/frontend/src/models/oss.ts b/rsconcept/frontend/src/models/oss.ts index 9c426f8b..4332a8ca 100644 --- a/rsconcept/frontend/src/models/oss.ts +++ b/rsconcept/frontend/src/models/oss.ts @@ -3,17 +3,65 @@ */ import { ILibraryItemData } from './library'; +import { UserID } from './user'; +import { IConstituenta, ISubstitution } from '@/models/rsform.ts'; /** * Represents backend data for Schema of Synthesis Operations. */ 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; } /** * Represents Schema of Synthesis Operations. */ export interface IOperationSchema extends IOperationSchemaData { - producedData: number[]; // TODO: modify this to store calculated state on load -} + subscribers: UserID[]; + editors: UserID[]; diff --git a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx index a42d4e6e..eef62e28 100644 --- a/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx +++ b/rsconcept/frontend/src/pages/OssPage/EditorOssGraph/EditorOssGraph.tsx @@ -1,13 +1,23 @@ 'use client'; +import { ReactFlowProvider } from '@reactflow/core'; + +import SynthesisFlow from '@/components/ui/Synthesis/SynthesisFlow.tsx'; import AnimateFade from '@/components/wrap/AnimateFade'; +import { SynthesisState } from '@/pages/OssPage/SynthesisContext.tsx'; +import SynthesisToolbar from '@/pages/OssPage/SynthesisToolbar.tsx'; function EditorOssGraph() { // TODO: Implement OSS editing UI here return ( -
Реализация графического интерфейса
+ + + + + +
); } diff --git a/rsconcept/frontend/src/pages/OssPage/SynthesisContext.tsx b/rsconcept/frontend/src/pages/OssPage/SynthesisContext.tsx new file mode 100644 index 00000000..7b5a96ce --- /dev/null +++ b/rsconcept/frontend/src/pages/OssPage/SynthesisContext.tsx @@ -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>) => void, + +} + +interface IBoundMap { + nodeId: string, + rsformId: number +} + +const SynthesisContext = createContext(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 '); + } + return context; +}; + +export const SynthesisState = ({ synthesisSchemaID, children }: SynthesisStateProps) => { + const [showSynthesisModal, setShowSynthesisModal] = useState(false); + const [showSelectInputModal, setShowSelectInputModal] = useState(false); + const [selectedNode, setSelectedNode] = useState(null); + + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + + const [bounds, setBounds] = useState([]); + const [substitutionBounds, setSubstitutionBounds] = useState([]); + const [substitutions, setSubstitutions] = useState([]); + 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> + ) => { + 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 ( + 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 ? ( setShowSynthesisModal(false)} + nodeId={selectedNode?.id} + onSynthesis={() => singleSynthesis} + />) : null} + {showSelectInputModal ? ( setShowSelectInputModal(false)} + />) : null} + {children} + + + ); +}; \ No newline at end of file diff --git a/rsconcept/frontend/src/pages/OssPage/SynthesisOperation.tsx b/rsconcept/frontend/src/pages/OssPage/SynthesisOperation.tsx new file mode 100644 index 00000000..4cca2467 --- /dev/null +++ b/rsconcept/frontend/src/pages/OssPage/SynthesisOperation.tsx @@ -0,0 +1,9 @@ + + + +function SynthesisOperation = () => { + return (
+ +
+ ) +} \ No newline at end of file diff --git a/rsconcept/frontend/src/pages/OssPage/SynthesisSubstitutionsTab.tsx b/rsconcept/frontend/src/pages/OssPage/SynthesisSubstitutionsTab.tsx new file mode 100644 index 00000000..1cb6cc67 --- /dev/null +++ b/rsconcept/frontend/src/pages/OssPage/SynthesisSubstitutionsTab.tsx @@ -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>; +} + +function SynthesisSubstitutionsTab({ + source, + receiver, + error, + substitutions, + setSubstitutions + }: SynthesisSubstitutionsTabProps) { + return ( + + + + ); +} + +export default SynthesisSubstitutionsTab; diff --git a/rsconcept/frontend/src/pages/OssPage/SynthesisToolbar.tsx b/rsconcept/frontend/src/pages/OssPage/SynthesisToolbar.tsx new file mode 100644 index 00000000..3ebb530f --- /dev/null +++ b/rsconcept/frontend/src/pages/OssPage/SynthesisToolbar.tsx @@ -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 ( + +