From dfc66876bfa9f155235a7a694d154f3cfe02079b Mon Sep 17 00:00:00 2001
From: dcz <dcz@ipipan.waw.pl>
Date: Mon, 23 Oct 2023 09:46:24 +0200
Subject: [PATCH] First commit.

---
 entries/templates/entries_base.html           |   1 +
 entries/templates/lu_free.html                |  24 +
 entries/urls.py                               |   1 +
 entries/views.py                              |  21 +
 .../components/unification/free_lu/FreeLu.vue |  73 +++
 .../unification/free_lu/FreeLuEdit.vue        | 556 ++++++++++++++++++
 .../unification/free_lu/FreeLuElement.vue     | 188 ++++++
 .../unification/free_lu/FreeLuPreview.vue     |  64 ++
 .../unification/free_lu/FreeLuRightPane.vue   |  68 +++
 frontend/src/main.js                          |   2 +
 10 files changed, 998 insertions(+)
 create mode 100644 entries/templates/lu_free.html
 create mode 100644 frontend/src/components/unification/free_lu/FreeLu.vue
 create mode 100644 frontend/src/components/unification/free_lu/FreeLuEdit.vue
 create mode 100644 frontend/src/components/unification/free_lu/FreeLuElement.vue
 create mode 100644 frontend/src/components/unification/free_lu/FreeLuPreview.vue
 create mode 100644 frontend/src/components/unification/free_lu/FreeLuRightPane.vue

diff --git a/entries/templates/entries_base.html b/entries/templates/entries_base.html
index 2e27f1d..85d9c3b 100644
--- a/entries/templates/entries_base.html
+++ b/entries/templates/entries_base.html
@@ -33,6 +33,7 @@
     <li class="nav-item mr-1"><a
                             onclick='window.location.replace(window.currUnifiedFrameId ? "/pl/entries/hierarchy/?unified_frame_id="+window.currUnifiedFrameId : "/pl/entries/hierarchy")'
                             class="nav-link cursor-pointer">{% trans "Hierarchia" %}</a></li>
+    <li class="nav-item mr-1"><a href="{% url 'entries:lu_free' %}" class="nav-link">{% trans "Wolne jednostki" %}</a></li>
 
 {% endif %}
 <li class="nav-item dropdown mr-1">
diff --git a/entries/templates/lu_free.html b/entries/templates/lu_free.html
new file mode 100644
index 0000000..61a407d
--- /dev/null
+++ b/entries/templates/lu_free.html
@@ -0,0 +1,24 @@
+{% extends "entries_base.html" %}
+
+{% load i18n %}
+{% load static %}
+
+{% block title %}{% trans "Hasła" %}{% endblock %}
+
+{% block scripts %}
+    {{ block.super }}
+    <link rel="stylesheet" type="text/css" href="{% static 'entries/css/unification_frames.css' %}">
+    <link rel="stylesheet" type="text/css" href="{% static 'common/css/role_colours.css' %}">
+    <script src="{% static 'entries/js/unification_entries_list.js' %}"></script>
+    <script src="{% static 'entries/js/unification_frames_list.js' %}"></script>
+    <script src="{% static 'entries/js/unification_entries_for_frames_list.js' %}"></script>
+    <script src="{% static 'entries/js/jquery-impromptu.min.js' %}"></script>
+    <script>
+        window.currUnifiedFrameId = {{ unified_frame_id|default:'null' }};
+    </script>
+{% endblock %}
+
+{% block modals %}
+    {{ block.super }}
+    <div id="lexical-unit-notes-template" class="d-none">{% include 'notes.html' %}</div>
+{% endblock %}
diff --git a/entries/urls.py b/entries/urls.py
index 445b935..3ff50d6 100644
--- a/entries/urls.py
+++ b/entries/urls.py
@@ -19,6 +19,7 @@ urlpatterns = [
     path('change_show_linked_entries/', views.change_show_linked_entries, name='change_show_linked_entries'),
     path('unification/', views.unification, name='unification'),
     path('hierarchy/', views.hierarchy, name='hierarchy'),
+    path('lu_free/', views.lu_free, name='lu_free'),
 
     path('autocomplete/', autocompletes.autocomplete, name='autocomplete'),
     path('plWN_context_lookup/', ajax_plWN_context_lookup, name='plWN_context_lookup'),
diff --git a/entries/views.py b/entries/views.py
index ce69c16..ee21da5 100644
--- a/entries/views.py
+++ b/entries/views.py
@@ -113,6 +113,27 @@ def hierarchy(request):
     )
 
 
+@login_required
+def lu_free(request):
+
+    unified_frame_id = None
+    if "unified_frame_id" in request.GET:
+        unified_frame_id = request.GET.get("unified_frame_id")
+
+    return render(
+        request,
+        'lu_free.html',
+        {
+            'is_vue_app': True,
+            'unified_frame_id': unified_frame_id,
+            'entries_form': EntryForm(),
+            'frames_form': FrameFormFactory.get_form(as_subform=False),
+            'schemata_form': SchemaFormFactory.get_form(as_subform=False),
+            'unified_frames_form': UnifiedFrameFormFactory.get_form(as_subform=False),
+        },
+    )
+
+
 FORM_TYPES = {
     'entry'      : EntryForm,
 }
diff --git a/frontend/src/components/unification/free_lu/FreeLu.vue b/frontend/src/components/unification/free_lu/FreeLu.vue
new file mode 100644
index 0000000..280e9d2
--- /dev/null
+++ b/frontend/src/components/unification/free_lu/FreeLu.vue
@@ -0,0 +1,73 @@
+<script>
+import UnificationFramesList from "../Unification/UnificationFramesList.vue";
+import FreeLuRightPane from "./FreeLuRightPane.vue";
+
+export default {
+  data () {
+    return {
+      entryId: null,
+      lexicalUnitId: null,
+      unifiedFrameId: null,
+      gettext: window.gettext,
+      unificationEntriesListRefreshKey: 1,
+    };
+  },
+  components: {FreeLuRightPane, UnificationFramesList},
+  methods: {
+    lexicalUnitSelected (entryId, lexicalUnitId) {
+      this.entryId = entryId;
+      this.lexicalUnitId = lexicalUnitId;
+    },
+    unifiedFrameSelected (unifiedFrameId, entryId, lexicalUnitId) {
+      this.unifiedFrameId = unifiedFrameId;
+      this.entryId = entryId;
+      this.lexicalUnitId = lexicalUnitId;
+    },
+    refreshEntriesList() {
+      this.unificationEntriesListRefreshKey++;
+    }
+  },
+  setup() {
+    const unified_frame_id = new URL(location.href).searchParams.get('unified_frame_id');
+    return {
+      initial_unified_frame_id: parseInt(unified_frame_id),
+    };
+  },
+  mounted () {
+    this.unifiedFrameSelected(window.currUnifiedFrameId);
+    $('#entries-list').length && Split(['#entries-list', '#entry-display'], {
+      sizes: [20, 80],
+      gutterSize: 4,
+      minSize: 10,
+      elementStyle: (dimension, size, gutterSize) => {
+        return {
+          'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)'
+        }
+      },
+    });
+  },
+};
+</script>
+
+<template>
+  <div id="entries-list" class="col h-100 w-100 pr-0 overflow-hidden">
+    <div id="entries-list-div" class="col p-0 h-100 w-100 overflow-hidden">
+      <unification-frames-list
+              :unificationEntriesListRefreshKey="unificationEntriesListRefreshKey"
+              :initialUnifiedFrameId="initial_unified_frame_id"
+              :setupHierarchyMarking="true"
+              @unified-frame-selected="unifiedFrameSelected"
+      />
+    </div>
+  </div>
+  <div id="entry-display" class="col h-100 p-0 overflow-hidden">
+    <free-lu-right-pane
+        ref="hierarchyRightPane"
+        :entryId="entryId"
+        :lexicalUnitId="lexicalUnitId"
+        :initialUnifiedFrameId="unifiedFrameId"
+        @refresh-entries-list="refreshEntriesList"
+    />
+  </div>
+</template>
+
diff --git a/frontend/src/components/unification/free_lu/FreeLuEdit.vue b/frontend/src/components/unification/free_lu/FreeLuEdit.vue
new file mode 100644
index 0000000..58881e1
--- /dev/null
+++ b/frontend/src/components/unification/free_lu/FreeLuEdit.vue
@@ -0,0 +1,556 @@
+<script>
+import InfoTooltip from "../../shared/InfoTooltip.vue";
+import Spinner from "../../shared/Spinner.vue";
+import ExamplesComponent from "../shared/frame-components/ExamplesComponent.vue";
+import SlowalFrameComponent from "../shared/frame-components/SlowalFrameComponent.vue";
+import SemanticsSchemataComponent from "../shared/frame-components/SemanticsSchemataComponent.vue";
+import MeaningComponent from "../shared/frame-components/MeaningComponent.vue";
+import SelectionalPreference from "../Unification/SelectionalPreference.js";
+import { slowal_frames2selecional_preferencies } from "../shared/utils.js";
+import FreeLuPreview from "./FreeLuPreview.vue";
+import FreeLuElement from "./FreeLuElement.vue";
+
+let FreeLuEdit = {
+  components: {FreeLuElement, FreeLuPreview}
+};
+
+Object.assign(FreeLuEdit, {
+  props: {
+    unifiedFrameId: Number,
+    previewedUnifiedFrameId: Number,
+    readOnly: Boolean,
+    initialRightPaneTab: String,
+    forceRefresh: Number,
+  },
+  data() {
+    return {
+      gettext: window.gettext,
+      FreeLuEdit: FreeLuEdit,
+      unified_frame: {},
+      unified_frame_title: '',
+      unified_frame_arguments: [],
+      active_unified_frame_argument: null,
+      slowal_frames2selecional_preferencies_mapping: {},
+      lexical_units: [],
+      img_prefix: window.STATIC_URL,
+      frames: [],
+      right_pane_tabs: [
+        {id: 'schemata', label: gettext('Schematy')},
+        {id: 'frame_preview', label: gettext('PodglÄ…d ram')},
+        {id: 'hierarchy', label: gettext('Hierarchia')},
+        {id: 'notes', label: gettext('Notatki')},
+      ],
+      right_pane_tab: this.initialRightPaneTab || 'schemata',
+      currentPreviewedUnifiedFrameId: this.previewedUnifiedFrameId,
+      internalForceRefresh: this.forceRefresh,
+      statusButtonTitle: '',
+      active_slowal_frame: null,
+      showVerifiedFrames: false,
+      subentries: null,
+      alternations: null,
+      realisation_phrases: null,
+      realisation_descriptions: null,
+      examples: null,
+      selectedFrameArguments: null,
+      frame_arguments_or_type: false,
+      selectedLus: null,
+      selectedSchemas: null,
+      selectedExamples: null,
+      hidden_frames: [],
+      hierarchy_hyponyms: null,
+      hierarchy_hyperonyms: null,
+    }
+  },
+  components: {FreeLuElement, InfoTooltip, Spinner, FreeLuPreview, SlowalFrameComponent, ExamplesComponent, SemanticsSchemataComponent, MeaningComponent},
+  emits: ['goToDisplay', 'refresh', 'swapFrames', 'refreshEntriesList', 'clearUnifiedFrameView'],
+  watch: {
+    forceRefresh(newVal, oldVal) {
+      this.loadFrame();
+    }
+  },
+  computed: {
+    selectionalPreference() {
+      return new SelectionalPreference();
+    },
+  },
+  methods: {
+    createHierarchyTree() {
+      return {
+        'unified_frame': this.unified_frame,
+        'hyponyms': this.hierarchy_hyponyms,
+        'hypyronyms': this.hierarchy_hyperonyms
+      };
+    },
+    goToEdit (unifiedFrameId, entryId, lexicalUnitId) {
+      window.location = `/${lang}/entries/unification?unified_frame_id=${this.unified_frame.id}`;
+    },
+    hasWhiteSpace(s) {
+      return /\s/g.test(s);
+    },
+    async loadFrame() {
+      try {
+        const data = {'unified_frame_id': this.unifiedFrameId, 'no_filters' : false};
+        $.ajax({
+          type: 'post',
+          url: '/' + lang + '/unifier/get_unified_frame/',
+          dataType: 'json',
+          data: data,
+          timeout: 60000,
+          success: function (response) {
+
+            this.img_prefix = window.STATIC_URL;
+            this.lexical_units = this.frames2lexical_units(response.frames);
+            this.unified_frame = response.unified_frame;
+            this.unified_frame_title = this.unified_frame.title;
+            this.unified_frame_arguments = this.unified_frame.arguments;
+            this.frames = response.frames;
+            this.slowal_frames2selecional_preferencies_mapping = slowal_frames2selecional_preferencies(this.unified_frame, response.frames);
+
+            this.subentries = response.subentries;
+            this.alternations = response.alternations;
+            this.realisation_phrases = response.realisation_phrases;
+            this.realisation_descriptions = response.realisation_descriptions;
+            this.examples = response.examples;
+
+            this.fulfill_slowal_frames_arguments_with_empty_elems(response.unified_frame, response.frames)
+            window.update_last_visited(response.last_visited);
+            window.clear_info();
+
+            if (!this.active_slowal_frame) {
+              this.setup_notes_unified_frame();
+            }
+          }.bind(this),
+          error: function (request, errorType, errorMessage) {
+            show_error(errorType + ' (' + errorMessage + ')');
+          }
+        });
+        this.loadHierarchy();
+      } catch (error) {
+        console.log(error);
+      }
+    },
+    loadHierarchy() {
+      $.ajax({
+        type: 'post',
+        url: '/' + lang + '/unifier/get_hierarchy_hyponyms/'+this.unifiedFrameId+"/",
+        dataType: 'json',
+        timeout: 60000,
+        success: function (response) {
+          this.hierarchy_hyponyms = response.hyponyms;
+        }.bind(this),
+        error: function (request, errorType, errorMessage) {
+          show_error(errorType + ' (' + errorMessage + ')');
+        }
+      });
+
+      $.ajax({
+        type: 'post',
+        url: '/' + lang + '/unifier/get_hierarchy_hyperonyms/'+this.unifiedFrameId+"/",
+        dataType: 'json',
+        timeout: 60000,
+        success: function (response) {
+          this.hierarchy_hyperonyms = response.hyperonyms;
+        }.bind(this),
+        error: function (request, errorType, errorMessage) {
+          show_error(errorType + ' (' + errorMessage + ')');
+        }
+      });
+    },
+    setup_notes_unified_frame() {
+      setup_notes($('#notes-component'), $('#lexical-unit-notes-template'), this.unified_frame.id, 'unifier.UnifiedFrame',
+              this.setup_notes_unified_frame, 'hierarchy_frame', true);
+    },
+    unifiedFrameArgumentSelected(argument) {
+      if (this.active_unified_frame_argument === argument) {
+        this.active_unified_frame_argument = null;
+        this.frame_arguments_or_type = false;
+        this.deselectSlowalFrameSelectedElements();
+      } else {
+        this.active_slowal_frame = null;
+        this.active_unified_frame_argument = argument;
+        const slowalFrameArguments = this.getSlowalFrameArgumentsBy(argument);
+        this.frame_arguments_or_type = true;
+        this.deselectSlowalFrameSelectedElements();
+        this.selectedFrameArguments = slowalFrameArguments;
+      }
+      this.unifiedFrameArgumentHovered(argument);
+    },
+    unifiedFrameArgumentHovered(argument) {
+      clear_info();
+      if (argument && this.active_unified_frame_argument === argument) {
+        show_info(gettext('Kliknij, aby cofnąć wybór kolumny do edycji.'));
+      }
+      if (argument && this.active_unified_frame_argument !== argument) {
+        show_info(gettext('Kliknij, aby wybrać kolumnę do edycji.'));
+      }
+    },
+    create_hyponym_or_hyperonym_relation(isHyperonym) {
+      if (this.currentPreviewedUnifiedFrameId === -1) {
+        alert(gettext("Wybierz w oknie podglądu ram ramę, która będzie " + (isHyperonym ? "hiperonimem" : "hiponimem") + " ramy z okna głównego."));
+      } else {
+        const data = isHyperonym ? {'hyponym_id': this.currentPreviewedUnifiedFrameId, 'hyperonym_id': this.unified_frame.id} :
+                {'hyponym_id': this.unified_frame.id, 'hyperonym_id': this.currentPreviewedUnifiedFrameId};;
+        $.ajax({
+          type: 'post',
+          url: '/' + lang + '/unifier/hierarchy_assign/',
+          dataType: 'json',
+          data: data,
+          timeout: 60000,
+          success: function (response) {
+            if(response.succ === 'true') {
+              show_info('Relacja hierarchii została ustanowiona.');
+              this.loadHierarchy();
+              this.$emit('refreshEntriesList');
+            } else if(response.exists === 'true') {
+              alert(gettext("Wybrana relacja hierarchii już występuje."));
+            }
+          }.bind(this),
+          error: function (request, errorType, errorMessage) {
+            show_error(errorType + ' (' + errorMessage + ')');
+          }
+        });
+      }
+    },
+    delete_hyponym_or_hyperonym_relation() {
+      if (this.currentPreviewedUnifiedFrameId === -1) {
+        alert(gettext("Wybierz w oknie podglądu ram ramę, dla której powiązanie ma zostać usunięte."));
+      } else {
+        const data = {'rel1_id': this.currentPreviewedUnifiedFrameId, 'rel2_id': this.unified_frame.id};
+        $.ajax({
+          type: 'post',
+          url: '/' + lang + '/unifier/hierarchy_unassign/',
+          dataType: 'json',
+          data: data,
+          timeout: 60000,
+          success: function (response) {
+            if(response.succ === true) {
+              show_info('Relacja hierarchii została usunięta.');
+              this.loadHierarchy();
+              this.$emit('refreshEntriesList');
+            } else {
+              alert(gettext("Wybrana relacja hierarchii nie występuje."));
+            }
+          }.bind(this),
+          error: function (request, errorType, errorMessage) {
+            show_error(errorType + ' (' + errorMessage + ')');
+          }
+        });
+      }
+    },
+    getSlowalFrameArgumentsBy(unified_frame_argument) {
+      const slowalFrameArgumentIds = [];
+      for (let i in this.unified_frame.slowal_frame_mapping) {
+        const slowal_frame_mapping = this.unified_frame.slowal_frame_mapping[i];
+        const slowalFrame = this.frames.find(frame => frame.id === slowal_frame_mapping.slowal_frame_id);
+        if(slowalFrame != null) {
+          for (let j in slowal_frame_mapping.slowal_frame_argument_mapping) {
+            const slowal_frame_argument_mapping = slowal_frame_mapping.slowal_frame_argument_mapping[j];
+            if (slowal_frame_argument_mapping.unified_frame_agrument_id == unified_frame_argument.id) {
+              const slowalFrameArgument = slowalFrame.arguments.find(arg => arg.argument_id === slowal_frame_argument_mapping.slowal_frame_agrument_id);
+              slowalFrameArgumentIds.push(slowalFrameArgument);
+            }
+          }
+        }
+      }
+      return slowalFrameArgumentIds;
+    },
+    isSuperLeksykograf() {
+      return has_permission("users.view_assignment");
+    },
+    isFrameVerified(frame) {
+      const isSuperLeksykograf = this.isSuperLeksykograf();
+      return (!isSuperLeksykograf && frame.status === 'G') || (isSuperLeksykograf && frame.status === 'S')
+    },
+    select_slowal_frame_req(to_invoke) {
+      if (this.active_slowal_frame) {
+        to_invoke();
+      } else {
+        alert(gettext("Wybierz ramę, dla której chcesz zmienić status."));
+      }
+    },
+    isFrameVisible(status) {
+      return (status != 'B' && status != 'C') || this.isSuperLeksykograf();
+    },
+    deselectSlowalFrameSelectedElements() {
+      this.subentries.forEach(subentry => {
+        subentry.schemata.forEach(s => {
+          s.selected = false;
+        });
+      });
+      this.frames.forEach(frame => {
+        frame.lexical_units.forEach(lu => {
+          lu.selected = false;
+        });
+        frame.arguments.forEach(argument => {
+          argument.selected = false;
+        });
+      });
+      this.examples.forEach(example => {
+        example.selected = false;
+      });
+      this.selectedLus = [];
+      this.selectedFrameArguments = [];
+      this.selectedSchemas = [];
+      this.selectedExamples = [];
+    },
+    slowalFrameSelected(frame) {
+      this.deselectSlowalFrameSelectedElements();
+      if (this.active_slowal_frame === frame) {
+        this.active_slowal_frame = null;
+      } else {
+        this.active_slowal_frame = frame;
+      }
+    },
+    isSelectedFrame(frame) {
+      if (this.active_slowal_frame) {
+        return frame.id === this.active_slowal_frame.id;
+      } else {
+        return false;
+      }
+    },
+    changePreviewedUnifiedFrameId(unifiedFrameId) {
+      this.currentPreviewedUnifiedFrameId = unifiedFrameId;
+    },
+    getArgumentCSS(argument) {
+      return (argument.role ? argument.role.str + ' ' : '') + (argument == this.active_unified_frame_argument ? 'active' : '');
+    },
+    schemataSelected(schemas) {
+      this.selectedSchemas = schemas;
+    },
+    exampleSelected(selectedExamples) {
+      this.selectedExamples = selectedExamples;
+    },
+    isReadOnlyForSuperLeksykograf() {
+      return (this.isSuperLeksykograf() && this.unified_frame.status === 'O') && this.unified_frame.assignee_username !== window.USER_USERNAME;
+    },
+    frames2lexical_units(frames) {
+      const lexical_units = []
+      for (let i in frames) {
+        const frame = frames[i];
+        for (let j in frame.lexical_units) {
+          const lexical_unit = frame.lexical_units[j];
+          lexical_unit.opinion = frame.opinion;
+          lexical_unit.opinion_key = frame.opinion_key;
+          lexical_unit.frame_status = frame.status;
+          lexical_unit.frame = frame;
+          lexical_units.push(lexical_unit);
+        }
+      }
+      return lexical_units;
+    },
+    fulfill_slowal_frames_arguments_with_empty_elems(unified_frame, slowal_frames) {
+      for (let i in unified_frame.slowal_frame_mapping) {
+        const slowal_frame_mapping = unified_frame.slowal_frame_mapping[i];
+        let slowal_frame = slowal_frames.find(o => o.id === slowal_frame_mapping.slowal_frame_id);
+        if(slowal_frame != null) {
+          let new_slowal_frame_arguments = [];
+          for (let j in unified_frame.arguments) {
+            const unified_frame_argument = unified_frame.arguments[j];
+            let unified_frame_argument_mapping = slowal_frame_mapping.slowal_frame_argument_mapping.find(o => o.unified_frame_agrument_id === unified_frame_argument.id);
+            let slowal_frame_argument = null;
+            if (unified_frame_argument_mapping == null) {
+              slowal_frame_argument = {
+                'str': 'Empty',
+                'id': slowal_frame.id + '-_' + (unified_frame_argument.id),
+                'role': 'Empty',
+                'role_type': 'Empty',
+                'preferences': [],
+                'proposed_roles': [],
+              }
+            } else {
+              slowal_frame_argument = slowal_frame.arguments.find(o => o.argument_id === unified_frame_argument_mapping.slowal_frame_agrument_id);
+            }
+            new_slowal_frame_arguments.push(slowal_frame_argument)
+          }
+          slowal_frame.arguments = new_slowal_frame_arguments;
+        }
+      }
+    },
+    createFrameHierarchyRepresentationHTML(unified_frame) {
+
+    },
+    createHierarchyHTML() {
+      this.unified_frame
+    }
+  },
+  mounted() {
+
+    if(this.unifiedFrameId) {
+      this.loadFrame();
+    }
+
+    if(!this.readOnly) {
+      Split(['#semantics-frames-pane', '#semantics-schemata-pane'], {
+        sizes: [40, 60],
+        minSize: 10,
+        gutterSize: 4,
+        elementStyle: (dimension, size, gutterSize) => {
+          return {
+            'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)'
+          }
+        },
+      });
+      Split(['#semantics-unified-frame-pane', '#examples'], {
+        sizes: [60, 40],
+        direction: 'vertical',
+        gutterSize: 4,
+        minSize: 10,
+      });
+    } else {
+      Split(['#semantics-unified-frame-pane-preview', '#examples-preview'], {
+        sizes: [60, 40],
+        direction: 'vertical',
+        gutterSize: 4,
+        minSize: 10,
+      });
+    }
+  }
+});
+
+export default FreeLuEdit;
+
+</script>
+
+<template>
+    <div class="col h-100 px-0 pt-0 pb-0 overflow-auto" id="semantics-frames-pane">
+      <div :id="'semantics-unified-frame-pane' + (readOnly ? '-preview' : '')" class="col w-100 p-0 overflow-auto">
+        <table v-if="!readOnly && !isReadOnlyForSuperLeksykograf()" class="table-button-menu sticky-top" cellspacing="1">
+          <tr style="background-color: white;">
+            <td id="change-title" @click="create_hyponym_or_hyperonym_relation(false)" style="padding: 10px 15px 10px 15px; color: #000000;">Nadrama</td>
+            <td id="add-arg" @click="create_hyponym_or_hyperonym_relation(true)" style="padding: 10px 15px 10px 15px; color: #000000;">Podrama</td>
+            <td style="padding: 10px 15px 10px 15px; color: #000000;" @click="delete_hyponym_or_hyperonym_relation">Usuń powiązanie</td>
+            <td style="padding: 10px 15px 10px 15px; color: #000000;" @click="goToEdit">Edytuj</td>
+          </tr>
+        </table>
+        
+        <spinner />
+        <div align="center">
+        <div align="left" style="display: table;">
+          <div class="unifiedFrame mt-3" v-bind:data-frame_id="unified_frame.id" id="unified-frame-title" v-html="unified_frame_title"></div>
+          <table v-if="unified_frame.id" id="unified-frame" class="m-0 table-borderless border border-secondary text-dark frame active">
+          <tbody>
+            <tr>
+              <template v-for="argument in unified_frame_arguments">
+              <td
+                class="argument py-2 px-1 border-top border-left border-secondary role-column"
+                    :class="getArgumentCSS(argument)"
+                    @click="unifiedFrameArgumentSelected(argument)"
+                    @mouseover="unifiedFrameArgumentHovered(argument)"
+                    @mouseleave="unifiedFrameArgumentHovered(null)"
+                >
+                {{ argument.role_type }}
+                
+                <div
+                  v-if="argument.role"
+                >
+                  [{{ argument.role.str }}]
+                </div>
+                <div v-else>
+                  <ul class="ul-role">
+                    <li v-for="proposed_role in argument.proposed_roles">
+                      {{ proposed_role.str }}
+                    </li>
+                  </ul>
+                </div>
+              </td>
+              </template>
+              
+              
+            </tr>
+            <tr>
+              <td class="preferences py-0 px-0 border-top border-left border-secondary role-column align-top"
+                  v-for='argument in unified_frame_arguments'
+                  :key='argument.id'
+              >
+                <ul class="ul-preference" v-if="argument.preferences.length > 0">
+                  <li v-for='preference in argument.preferences'>
+                    <div
+                      v-if="preference.url != null"
+                      class="preference py-2 px-1 preference-bold"
+                    >
+                      <a class="synset-plwn" v-bind:href="preference.url" target="_blank">{{ preference.str }}</a>
+                    </div>
+                    <div v-else class="preference py-2 px-1 preference-bold">{{ preference.str }}</div>
+                  </li>
+                </ul>
+                <ul class="ul-preference" v-if="unified_frame.status !== 'S'">
+                  <li v-for="preference in slowal_frames2selecional_preferencies_mapping[argument.id]">
+                    <span v-if="preference.url != null" class="preference py-2 px-1">
+                      <a class="synset-plwn" v-bind:href="preference.url" target="_blank">{{ preference.str }}</a>
+                    </span>
+                    <span v-else class="preference py-2 px-1">{{ preference.str }}</span>
+                    <info-tooltip v-if="preference.info" :text="preference.info" />
+                  </li>
+                </ul>
+              </td>
+            </tr>
+          </tbody>
+        </table>
+        </div>
+        <div v-if="unified_frame.id" class="lu-table mt-3 mb-3">
+            <table class="m-0 table-borderless border border-secondary text-dark">
+              <tbody>
+                <tr>
+                  <th scope="row" class="py-2 px-1 text-secondary">Jednostka leksykalna</th>
+                  <th scope="row" class="py-2 px-1 text-secondary">Opinia</th>
+                  <th scope="row" class="py-2 px-1 text-secondary">Status</th>
+                </tr>
+                <tr class="preferences py-0 px-0 border-top border-left border-secondary"
+                  v-for='lexical_unit in lexical_units'
+                >
+                  <template v-if="isFrameVisible(lexical_unit.frame)">
+                    <td class="cursor-pointer argument py-2 px-1 border-top border-left border-secondary"
+                        @mouseenter="lexical_unit.frame.hover=true"
+                        @mouseleave="lexical_unit.frame.hover=false"
+                        :class="lexical_unit.frame === active_slowal_frame ? 'active-lu' : lexical_unit.frame.hover ? 'lu-underline' : ''"
+                        @click="slowalFrameSelected(lexical_unit.frame)">{{ lexical_unit.str }}</td>
+                    <td class="argument py-2 px-1 border-top border-left border-secondary">
+                      <img v-bind:src="img_prefix + 'entries/img/' +lexical_unit.opinion_key + '.svg'" width="12" height="12" v-bind:alt="lexical_unit.opinion">
+                      {{ lexical_unit.opinion }}
+                    </td>
+                    <td class="argument py-2 px-1 border-top border-left border-secondary">[{{ lexical_unit.frame_status }}]</td>
+                  </template>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+      </div>
+      </div>
+      <div :id="'examples' + (readOnly ? '-preview' : '')" class="col w-100 p-0 tab-pane overflow-auto">
+         <examples-component v-if="examples"
+              :examples="examples" 
+              :frame="active_slowal_frame"
+              :frame_arguments="selectedFrameArguments"
+              :frame_arguments_or_type="frame_arguments_or_type"
+              :lus="selectedLus"
+              :schemas="selectedSchemas"
+              :key="examples"
+              @example-selected="exampleSelected"
+          />
+        </div>
+    </div>
+    <div v-if="!readOnly" class="col h-100 px-1 pt-0 pb-0 overflow-auto" style="padding-left: 0px!important; padding-right: 0px!important;" id="semantics-schemata-pane">
+      <ul class="nav nav-pills nav-justified p-1" id="entryTabs" role="tablist">
+        <li
+          v-for="tab in right_pane_tabs"
+          class="btn btn-sm btn-outline-dark nav-link mx-1"
+          :class="right_pane_tab === tab.id && 'active'"
+          @click="right_pane_tab = tab.id"
+        >
+          {{ tab.label }}
+        </li>
+      </ul>
+      <div v-if="active_slowal_frame" :class="right_pane_tab !== 'schemata' && 'd-none'">
+          <semantics-schemata-component 
+                :subentries="subentries"
+                :key="subentries"
+                :frame="active_slowal_frame"
+                :alternations="alternations"
+                :realisation_phrases="realisation_phrases"
+                :realisation_descriptions="realisation_descriptions"
+                :selectedExamples="selectedExamples"
+                @schemata-selected="schemataSelected"
+              />
+      </div>
+    </div>
+</template>
diff --git a/frontend/src/components/unification/free_lu/FreeLuElement.vue b/frontend/src/components/unification/free_lu/FreeLuElement.vue
new file mode 100644
index 0000000..5a2b4aa
--- /dev/null
+++ b/frontend/src/components/unification/free_lu/FreeLuElement.vue
@@ -0,0 +1,188 @@
+<script>
+
+export default {
+    name: 'FreeLuElement',
+    props: {
+        node: {
+            type: Object,
+            required: true
+        },
+        spacing: {
+            type: Number,
+            default: 0
+        },
+        spacing_elem_type: String
+    },
+    data() {
+        return {
+            showHyponyms: false,
+            showHypyronyms: false
+        }
+    },
+    computed: {
+        nodeMargin() {
+            return {
+                'margin-left': `${this.spacing}px`
+            }
+        },
+        hasHyponyms() {
+            const {hyponyms} = this.node
+            return hyponyms && hyponyms.length > 0
+        },
+        hasHypyronyms() {
+            const {hypyronyms} = this.node
+            return hypyronyms && hypyronyms.length > 0
+        },
+        toggleChildrenIcon() {
+            return this.showHypyronyms ? 'fas fa-angle-down' : 'fas fa-angle-right'
+        },
+        getSpacingElem() {
+            if(this.spacing_elem_type === "std") {
+                return "&#9474; &#9500;&#8594; &#9474;";
+            } else if(this.spacing_elem_type === "top_corner") {
+                return "&#160; &#160; &#160; &#160; &#9484;&#8594; &#9474; &#9474;";
+            } else if(this.spacing_elem_type === "down_corner") {
+                return "&#9474; &#9474; &#9492;&#8594;";
+            } else {
+                return "";
+            }
+        },
+    },
+    methods: {
+        loadChildren (children) {
+            for (let i in children) {
+                const hypyronym = children[i];
+                if (hypyronym.hasChildrenLoaded !== true) {
+                    $.ajax({
+                        type: 'post',
+                        url: '/' + lang + '/unifier/get_hierarchy_hyperonyms/' + hypyronym.unified_frame_id + "/",
+                        dataType: 'json',
+                        timeout: 60000,
+                        success: function (response) {
+                            hypyronym.hypyronyms = response.hyperonyms;
+                        }.bind(this),
+                        error: function (request, errorType, errorMessage) {
+                            show_error(errorType + ' (' + errorMessage + ')');
+                        }
+                    });
+                    $.ajax({
+                        type: 'post',
+                        url: '/' + lang + '/unifier/get_hierarchy_hyponyms/' + hypyronym.unified_frame_id + "/",
+                        dataType: 'json',
+                        timeout: 60000,
+                        success: function (response) {
+                            hypyronym.hyponyms = response.hyponyms;
+                        }.bind(this),
+                        error: function (request, errorType, errorMessage) {
+                            show_error(errorType + ' (' + errorMessage + ')');
+                        }
+                    });
+                }
+            }
+        }, toggleHypyronyms() {
+            this.loadChildren(this.node.hypyronyms);
+            this.showHypyronyms = !this.showHypyronyms
+        },
+        toggleHyponyms() {
+            this.loadChildren(this.node.hyponyms);
+            this.showHyponyms = !this.showHyponyms
+        }
+    }
+}
+
+</script>
+
+
+<template>
+    <div :style="nodeMargin" style="height: 100%">
+        <div v-if="hasHypyronyms" v-show="showHypyronyms">
+            <free-lu-element
+                    v-for="(child, index) in node.hypyronyms"
+                    :key="child.id"
+                    :node="child"
+                    :spacing_elem_type='index === 0 ? "top_corner" : "std"'
+                    :spacing="spacing + 40"
+            />
+        </div>
+        <div show class="d-flex justify-content-between mb-1">
+            <div class="row">
+                <div class="col" style="max-width: 10px" v-html="getSpacingElem"/>
+                <div class="col">
+                    <div class="row">
+                        <div class="col cursor-pointer" style="text-align: center; font-size: large"><span
+                                v-if="hasHypyronyms"
+                                :class="toggleChildrenIcon"
+                                @click="toggleHypyronyms"
+                                @keypress="toggleHypyronyms">&#9650;</span></div>
+                    </div>
+                    <div class="row">
+                        <div class="col" align="left" style="display: table;">
+                            <div class="unifiedFrame" id="hierarchy-unified-frame-title" v-html="node.unified_frame.title"></div>
+                            <table id="hierarchy-unified-frame" class="m-0 table-borderless border border-secondary text-dark frame active">
+                                <tbody>
+                                <tr>
+                                    <template v-for="argument in node.unified_frame.arguments">
+                                        <td
+                                                class="argument py-2 px-1 border-top border-left border-secondary role-column"
+                                                :class="argument.role ? argument.role.str + ' ' : ''"
+                                        >
+                                            {{ argument.role_type }}
+
+                                            <div
+                                                    v-if="argument.role"
+                                            >
+                                                [{{ argument.role.str }}]
+                                            </div>
+                                            <div v-else>
+                                                <ul class="ul-role">
+                                                    <li v-for="proposed_role in argument.proposed_roles">
+                                                        {{ proposed_role.str }}
+                                                    </li>
+                                                </ul>
+                                            </div>
+                                        </td>
+                                    </template>
+                                </tr>
+                                <tr>
+                                    <td hidden class="preferences py-0 px-0 border-top border-left border-secondary role-column align-top"
+                                        v-for='argument in node.unified_frame.arguments'
+                                        :key='argument.id'
+                                    >
+                                        <ul class="ul-preference" v-if="argument.preferences.length > 0">
+                                            <li v-for='preference in argument.preferences'>
+                                                <div
+                                                        v-if="preference.url != null"
+                                                        class="preference py-2 px-1 preference-bold"
+                                                >
+                                                    <a class="synset-plwn" v-bind:href="preference.url" target="_blank">{{ preference.str }}</a>
+                                                </div>
+                                                <div v-else class="preference py-2 px-1 preference-bold">{{ preference.str }}</div>
+                                            </li>
+                                        </ul>
+                                    </td>
+                                </tr>
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                    <div class="row align-content-center">
+                        <div class="col cursor-pointer" style="text-align: center; font-size: large"><span
+                                v-if="hasHyponyms"
+                                :class="toggleChildrenIcon"
+                                @click="toggleHyponyms"
+                                @keypress="toggleHyponyms">&#9660;</span></div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div v-if="hasHyponyms" v-show="showHyponyms">
+            <free-lu-element
+                    v-for="(child, index) in node.hyponyms"
+                    :key="child.id"
+                    :node="child"
+                    :spacing_elem_type='index === node.hyponyms.length - 1 ? "down_corner" : "std"'
+                    :spacing="spacing + 40"
+            />
+        </div>
+    </div>
+</template>
diff --git a/frontend/src/components/unification/free_lu/FreeLuPreview.vue b/frontend/src/components/unification/free_lu/FreeLuPreview.vue
new file mode 100644
index 0000000..a88db17
--- /dev/null
+++ b/frontend/src/components/unification/free_lu/FreeLuPreview.vue
@@ -0,0 +1,64 @@
+<script>
+import UnificationFramesList from "../Unification/UnificationFramesList.vue";
+
+export default {
+  props: {
+    initialUnifiedFrameId: Number,
+    initialLexicalUnitId: Number,
+    forceRefresh: Number,
+    hierarchyEditComponent: Object,
+  },
+  data () {
+    return {
+      unifiedFrameId: this.initialUnifiedFrameId
+    };
+  },
+  components: {UnificationFramesList},
+  emits: ['changeFrame', 'refreshEntriesList'],
+  methods: {
+    unifiedFrameSelected (unifiedFrameId) {
+      this.$emit('changeFrame', unifiedFrameId);
+    },
+    refreshEntriesList() {
+      this.$emit('refreshEntriesList')
+    },
+  },
+  mounted () {
+    Split(['#frame-preview-left-pane', '#frame-preview-right-pane'], {
+      sizes: [60, 40],
+      minSize: 20,
+      gutterSize: 4,
+      elementStyle: (dimension, size, gutterSize) => {
+        return {
+          'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)'
+        }
+      },
+    });
+  }
+};
+</script>
+
+<template>
+  <div class="row h-100 overflow-hidden">
+    <div :key="unifiedFrameId" class="col h-100 pr-0 pt-0 pb-0 overflow-auto" id="frame-preview-left-pane">
+      <component v-bind:is="hierarchyEditComponent"
+        v-if="unifiedFrameId !== -1"
+        :readOnly="true"
+        :unifiedFrameId="unifiedFrameId"
+        :forceRefresh="forceRefresh"
+        @refresh-entries-list="refreshEntriesList"
+      />
+      <div v-else class="h-100">
+        Brak ramy do wyświetlenia
+      </div>
+    </div>
+    <div class="col h-100 pl-1 pt-0 pb-0 overflow-auto" id="frame-preview-right-pane">
+<!--      <unification-switchable-list-->
+<!--        @unified-frame-selected="unifiedFrameSelected"-->
+<!--      />-->
+      <unification-frames-list
+            @unified-frame-selected="unifiedFrameSelected"
+      />
+    </div>
+  </div>
+</template>
diff --git a/frontend/src/components/unification/free_lu/FreeLuRightPane.vue b/frontend/src/components/unification/free_lu/FreeLuRightPane.vue
new file mode 100644
index 0000000..8372acd
--- /dev/null
+++ b/frontend/src/components/unification/free_lu/FreeLuRightPane.vue
@@ -0,0 +1,68 @@
+<script>
+import FreeLuEdit from './FreeLuEdit.vue';
+
+export default {
+  components: {FreeLuEdit},
+  props: {
+    entryId: Number,
+    lexicalUnitId: Number,
+    initialUnifiedFrameId: Number,
+  },
+  emits: ['refreshEntriesList'],
+  data() {
+    return this.getInitialData();
+  },
+  methods: {
+    getInitialData() {
+      return {
+        key: this.lexicalUnitId,
+        entryIdLocal: this.entryId,
+        unifiedFrameId: this.initialUnifiedFrameId,
+        previewedUnifiedFrameId: -1
+      };
+    },
+    goToDisplay() {
+      this.unifiedFrameId = null;
+    },
+    refresh() {
+      this.key = null;
+      setTimeout(() => {
+        this.key = this.lexicalUnitId;
+      }, 0);
+    },
+    swapFrames(previewedUnifiedFrameId) {
+      this.previewedUnifiedFrameId = this.unifiedFrameId;
+      this.unifiedFrameId = previewedUnifiedFrameId;
+      this.refresh();
+    },
+    refreshEntriesList() {
+      this.$emit('refreshEntriesList');
+    }
+  },
+  watch: {
+    lexicalUnitId() {
+      Object.assign(this, this.getInitialData());
+    },
+    initialUnifiedFrameId() {
+      Object.assign(this, this.getInitialData());
+    }
+  },
+};
+</script>
+
+<template>
+  <div v-if="key || unifiedFrameId" :key="(key, entryIdLocal, unifiedFrameId)" class="row h-100 m-0 p-0 overflow-auto" id="semantics-top-pane">
+    <free-lu-edit
+      ref="hierarchyEdit"
+      v-if="unifiedFrameId"
+      :key="unifiedFrameId"
+      :readOnly="false"
+      :unifiedFrameId="unifiedFrameId"
+      :previewedUnifiedFrameId="previewedUnifiedFrameId"
+      :initialRightPaneTab="previewedUnifiedFrameId && unifiedFrameId !== previewedUnifiedFrameId ? 'frame_preview' : 'schemata'"
+      @go-to-display="goToDisplay"
+      @swap-frames="swapFrames"
+      @refresh-entries-list="refreshEntriesList"
+    />
+  </div>
+</template>
diff --git a/frontend/src/main.js b/frontend/src/main.js
index 21c30df..6162fa0 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -6,6 +6,7 @@ import { createRouter, createWebHistory } from "vue-router";
 import Entries from "./components/unification/Entries/Entries.vue";
 import Unification from "./components/unification/Unification/Unification.vue";
 import Hierarchy from "./components/unification/hierarchy/Hierarchy.vue";
+import FreeLu from "./components/unification/free_lu/FreeLu.vue";
 
 const router = createRouter({
   history: createWebHistory(),
@@ -13,6 +14,7 @@ const router = createRouter({
     { path: '/:lang/entries/', component: Entries },
     { path: '/:lang/entries/unification/', component: Unification },
     { path: '/:lang/entries/hierarchy/', component: Hierarchy },
+    { path: '/:lang/entries/lu_free/', component: FreeLu },
     { path: '/:pathMatch(.*)*', component: null, name: '404' },
   ]
 });
-- 
GitLab