diff --git a/editor/editor.html b/editor/editor.html
new file mode 100644
index 0000000000000000000000000000000000000000..d45958ec1204a133442fedfbc3e2127ff5154ef0
--- /dev/null
+++ b/editor/editor.html
@@ -0,0 +1,57 @@
+<div>
+    <style>
+        section {
+            border: 1px lightgrey solid;
+            border-radius: 1px;
+            margin: 5px;
+            padding: 2.5px;
+            width: auto;
+        }
+
+        section > * {
+            margin: 2.5px;
+        }
+
+        .selected {
+            background-color: lightblue;
+        }
+    </style>
+
+    <script src="https://unpkg.com/force-graph"></script>
+    <!--<script src="../../dist/force-graph.js"></script>-->
+
+    <script>
+        const PLUGIN_PATH = "%WWW%";
+    </script>
+
+    <script src="%WWW%editor/js/graph.js"></script>
+    <script src="%WWW%editor/js/tools/tool.js"></script>
+    <script src="%WWW%editor/js/tools/selecttool.js"></script>
+    <script src="%WWW%editor/js/tools/collecttool.js"></script>
+    <script src="%WWW%editor/js/tools/deletetool.js"></script>
+    <script src="%WWW%editor/js/display.js"></script>
+    <script src="%WWW%editor/js/state.js"></script>
+    <script src="%WWW%editor/js/editor.js"></script>
+
+    <h1>Interface</h1>
+    <div id="2d-graph"></div>
+    <section id="toolbar"></section>
+    <section>
+        <h3 id="selected-item">Nothing selected</h3>
+        <ul id="selected-params"></ul>
+        <button onclick="saveCurrentNode()">Save NOT YET IMPLEMENTED</button>
+        <section>
+            <h4>Sources</h4>
+            <ul id="selected-sources"></ul>
+        </section>
+        <section>
+            <h4>Targets</h4>
+            <ul id="selected-targets"></ul>
+        </section>
+    </section>
+    <section>
+        <h3>Collected items</h3>
+        <ul id="selected-items"></ul>
+        <button onclick="state.clearSelectedItems()">Clear</button>
+    </section>
+</div>
diff --git a/editor/images/tools/collect.png b/editor/images/tools/collect.png
new file mode 100644
index 0000000000000000000000000000000000000000..623d3a978005133e87994a16e67dbe9ab7259875
Binary files /dev/null and b/editor/images/tools/collect.png differ
diff --git a/editor/images/tools/delete.png b/editor/images/tools/delete.png
new file mode 100644
index 0000000000000000000000000000000000000000..007f69a325d89d0d5e4eee3d2b0df7f8844c4d13
Binary files /dev/null and b/editor/images/tools/delete.png differ
diff --git a/editor/images/tools/select.png b/editor/images/tools/select.png
new file mode 100644
index 0000000000000000000000000000000000000000..840c4f5df6c058d208879fe7d5bfe906eab43015
Binary files /dev/null and b/editor/images/tools/select.png differ
diff --git a/editor/js/display.js b/editor/js/display.js
new file mode 100644
index 0000000000000000000000000000000000000000..de6379c917018e86e17b1c7f74888884670055b5
--- /dev/null
+++ b/editor/js/display.js
@@ -0,0 +1,140 @@
+const ID_TOOLBAR = "#toolbar";
+const ID_SELECTEDITEM = "#selected-item";
+const ID_SELECTED_PARAMS = "#selected-params";
+const ID_SELECTED_SOURCES = "#selected-sources";
+const ID_SELECTED_TARGETS = "#selected-targets";
+const ID_SELECTEDITEMS = "#selected-items";
+
+const DOM_LIST_ITEM = "li";
+
+const TOOL_ICON_SRC = PLUGIN_PATH + "editor/images/tools/";
+const TOOL_ICON_FORMAT = ".png";
+const TOOL_SELECTED_CLASS = "selected";
+
+class Display {
+    constructor(tools) {
+        this.tools = Object.values(tools);
+        this.previousTool = undefined;
+
+        this.renderToolbar(this.tools);
+    }
+
+    setSelectedTool(tool) {
+        var selectedTool = jQuery(Display.getToolId(tool));
+        selectedTool.addClass(TOOL_SELECTED_CLASS);
+
+        if (this.previousTool !== undefined) {
+            var previousTool = jQuery(Display.getToolId(this.previousTool));
+            previousTool.removeClass(TOOL_SELECTED_CLASS);
+        }
+
+        this.previousTool = tool;
+    }
+
+    renderToolbar(tools) {
+        this.fillDomList(ID_TOOLBAR, tools, this.toolRenderer);
+    }
+
+    static getToolId(tool) {
+        return ID_TOOLBAR + "-" + tool.getKey();
+    }
+
+    toolRenderer(tool) {
+        return (
+            '<button id="' +
+            Display.getToolId(tool).substr(1) + // Remove # from id
+            '"onclick="state.setTool(TOOLS.' +
+            tool.getKey() +
+            ')" title="' +
+            tool.getName() +
+            '"><img src="' +
+            TOOL_ICON_SRC +
+            tool.getKey() +
+            TOOL_ICON_FORMAT +
+            '"></button>'
+        );
+    }
+
+    setSelectedItem(item) {
+        jQuery(ID_SELECTEDITEM).html(Display.toStr(item));
+
+        var paramsDOM = jQuery(ID_SELECTED_PARAMS);
+        paramsDOM.empty();
+
+        var params = NODE_PARAMS;
+        if (item.link) {
+            params = LINK_PARAMS;
+        }
+
+        params.forEach((param) => {
+            paramsDOM.append(
+                "<" +
+                    DOM_LIST_ITEM +
+                    ">" +
+                    param +
+                    ' <textarea>' +
+                    (item[param] === undefined ? "" : item[param]) +
+                    '</textarea></' +
+                    DOM_LIST_ITEM +
+                    ">"
+            );
+        });
+
+        // Render Source and Target list
+        var sources = [];
+        var targets = [];
+        if (item.node) {
+            var nodes = graph.data[GRAPH_NODES];
+            for (var i = 0; i < nodes.length; i++) {
+                if (graph.existsLink(nodes[i][NODE_ID], item[NODE_ID])) {
+                    sources.push(nodes[i]);
+                } else if (graph.existsLink(item[NODE_ID], nodes[i][NODE_ID])) {
+                    targets.push(nodes[i]);
+                }
+            }
+        } else if (item.link) {
+            sources.push(item[LINK_SOURCE]);
+            targets.push(item[LINK_TARGET]);
+        }
+
+        this.fillDomList(ID_SELECTED_SOURCES, sources, this.graphItemRenderer);
+        this.fillDomList(ID_SELECTED_TARGETS, targets, this.graphItemRenderer);
+    }
+
+    setSelectedItems(items, itemsContext) {
+        this.fillDomList(ID_SELECTEDITEMS, items, this.graphItemRenderer);
+    }
+
+    graphItemRenderer(item) {
+        return (
+            "<" +
+            DOM_LIST_ITEM +
+            ">" +
+            Display.toStr(item) +
+            "</" +
+            DOM_LIST_ITEM +
+            ">"
+        );
+    }
+
+    fillDomList(listId, items, itemRenderer) {
+        var listCont = jQuery(listId);
+        listCont.empty();
+
+        items.forEach((i) => listCont.append(itemRenderer(i)));
+    }
+
+    static toStr(item) {
+        if (item.node) {
+            return item[NODE_LABEL] + " [" + item[NODE_ID] + "]";
+        } else if (item.link) {
+            return (
+                Display.toStr(item[LINK_SOURCE]) +
+                " <-> " +
+                Display.toStr(item[LINK_TARGET])
+            );
+        } else {
+            return "UNDEFINED";
+        }
+    }
+}
diff --git a/editor/js/editor.js b/editor/js/editor.js
new file mode 100644
index 0000000000000000000000000000000000000000..88b22de5bc943deccb16db4cbb9cd2e3b6ec224d
--- /dev/null
+++ b/editor/js/editor.js
@@ -0,0 +1,56 @@
+var state;
+var graphObj;
+
+window.onload = function () {
+    fetch(JSON_CONFIG)
+        .then((r) => {
+            return r.json();
+        })
+        .then((graphConfig) => {
+            state = new State();
+
+            graph.data = graphConfig;
+            graph.addIdentifiers();
+            load();
+
+            // Deactivate physics after a short delay
+            setTimeout(() => {
+                graph.stopPhysics();
+            }, STOP_PHYSICS_DELAY);
+        });
+};
+
+document.onkeydown = (e) => state.onKeyDown(e);
+document.onkeyup = (e) => state.onKeyUp(e);
+
+function downloadJson() {
+    // TODO: Clean up
+    // source: https://stackoverflow.com/a/42883108/7376120
+    var jsonBlob = new Blob([JSON.stringify(getOnlygraph.data())], {
+        type: "application/json;charset=utf-8",
+    });
+    var link = window.URL.createObjectURL(jsonBlob);
+    window.location = link;
+}
+
+function load() {
+    const graphContainer = document.getElementById("2d-graph");
+    const width = graphContainer.offsetWidth;
+
+    graphObj = ForceGraph()(graphContainer)
+        .height(600)
+        .width(width)
+        .graphData(graph.data)
+        .nodeLabel(NODE_LABEL)
+        .nodeAutoColorBy(NODE_GROUP)
+        .onNodeClick((node) => state.onNodeClick(node))
+        .autoPauseRedraw(false) // keep redrawing after engine has stopped
+        .linkWidth((link) => state.linkWidth(link))
+        .linkDirectionalParticles(state.linkDirectionalParticles())
+        .linkDirectionalParticleWidth((link) =>
+            state.linkDirectionalParticleWidth(link)
+        )
+        .nodeCanvasObjectMode((node) => state.nodeCanvasObjectMode(node))
+        .nodeCanvasObject((node, ctx) => state.nodeCanvasObject(node, ctx))
+        .onLinkClick((link) => state.onLinkClick(link));
+}
diff --git a/editor/js/graph.js b/editor/js/graph.js
new file mode 100644
index 0000000000000000000000000000000000000000..52fffdc693061eeefdfa1362a3c78a6235c39fd1
--- /dev/null
+++ b/editor/js/graph.js
@@ -0,0 +1,162 @@
+const NODE_LABEL = "name";
+const NODE_ID = "id";
+const NODE_GROUP = "group";
+const NODE_DESCRIPTION = "description";
+const NODE_IMAGE = "image";
+
+const LINK_SOURCE = "source";
+const LINK_TARGET = "target";
+const LINK_TYPE = "type";
+const LINK_PARTICLE_COUNT = 4;
+
+const GRAPH_NODES = "nodes";
+const GRAPH_LINKS = "links";
+
+const IMAGE_SIZE = 12;
+const IMAGE_SRC = PLUGIN_PATH + "datasets/images/"
+
+const LINK_PARAMS = [LINK_TYPE];
+const NODE_PARAMS = [NODE_ID, NODE_LABEL, NODE_IMAGE, NODE_DESCRIPTION];
+
+const JSON_CONFIG = PLUGIN_PATH + "datasets/aud1.json";
+
+const STOP_PHYSICS_DELAY = 5000; // ms
+
+
+
+const graph = {
+    data: undefined,
+
+    deleteNode(nodeId) {
+        // Delete node from nodes
+        graph.data[GRAPH_NODES] = graph.data[GRAPH_NODES].filter(
+            (n) => n[NODE_ID] !== nodeId
+        );
+
+        // Delete links with node
+        graph.data[GRAPH_LINKS] = graph.data[GRAPH_LINKS].filter(
+            (l) =>
+                l[LINK_SOURCE][NODE_ID] !== nodeId &&
+                l[LINK_TARGET][NODE_ID] !== nodeId
+        );
+    },
+
+    stopPhysics() {
+        graph.data[GRAPH_NODES].forEach((n) => {
+            n.fx = n.x;
+            n.fy = n.y;
+        });
+    },
+
+    addIdentifiers() {
+        graph.data[GRAPH_NODES].forEach((n) => {
+            n.node = true;
+            n.link = false;
+        });
+        graph.data[GRAPH_LINKS].forEach((l) => {
+            l.node = false;
+            l.link = true;
+        });
+    },
+
+    deleteLink(sourceId, targetId) {
+        // Only keep links, of one of the nodes is different
+        graph.data[GRAPH_LINKS] = graph.data[GRAPH_LINKS].filter(
+            (l) =>
+                l[LINK_SOURCE][NODE_ID] !== sourceId ||
+                l[LINK_TARGET][NODE_ID] !== targetId
+        );
+    },
+
+    isLinkOnNode(link, node) {
+        if (link === undefined || node === undefined) {
+            return false;
+        }
+
+        if (link.link !== true || node.node !== true) {
+            return false;
+        }
+
+        return (
+            link[LINK_SOURCE][NODE_ID] === node[NODE_ID] ||
+            link[LINK_TARGET][NODE_ID] === node[NODE_ID]
+        );
+    },
+
+    existsLink(sourceId, targetId) {
+        const links = graph.data[GRAPH_LINKS];
+
+        for (var i = 0; i < links.length; i++) {
+            var link = links[i];
+            if (
+                link[LINK_SOURCE][NODE_ID] === sourceId &&
+                link[LINK_TARGET][NODE_ID] === targetId
+            ) {
+                return true;
+            }
+        }
+
+        return false;
+    },
+
+    connectNodes(sourceId, targetIds) {
+        targetIds.forEach((targetId) => {
+            if (
+                graph.existsLink(sourceId, targetId) ||
+                graph.existsLink(targetId, sourceId)
+            ) {
+                return;
+            }
+
+            var link = {};
+
+            link[LINK_SOURCE] = sourceId;
+            link[LINK_TARGET] = targetId;
+
+            graph.data[GRAPH_LINKS].push(link);
+        });
+    },
+
+    getCleanData() {
+        var cleanData = {};
+        cleanData[GRAPH_LINKS] = [];
+        cleanData[GRAPH_NODES] = [];
+
+        graph.data[GRAPH_LINKS].forEach((link) =>
+            cleanData[GRAPH_LINKS].push(graph.getCleanLink(link))
+        );
+
+        graph.data[GRAPH_NODES].forEach((node) =>
+            cleanData[GRAPH_NODES].push(graph.getCleanNode(node))
+        );
+
+        console.log(cleanData);
+        return cleanData;
+    },
+
+    getCleanNode(node) {
+        var cleanNode = {};
+
+        NODE_PARAMS.forEach((param) => {
+            cleanNode[param] = node[param];
+        });
+
+        return cleanNode;
+    },
+
+    getCleanLink(link) {
+        var cleanLink = {};
+
+        // Source and target nodes
+        // Node ids will be converted to complete node objects on running graphs, gotta convert back
+        cleanLink[LINK_SOURCE] = link[LINK_SOURCE][NODE_ID];
+        cleanLink[LINK_TARGET] = link[LINK_TARGET][NODE_ID];
+
+        // Other parameters
+        LINK_PARAMS.forEach((param) => {
+            cleanLink[param] = link[param];
+        });
+
+        return cleanLink;
+    },
+};
diff --git a/editor/js/state.js b/editor/js/state.js
new file mode 100644
index 0000000000000000000000000000000000000000..dc020c27adce29e3f81c9390d7d160becec92cd1
--- /dev/null
+++ b/editor/js/state.js
@@ -0,0 +1,198 @@
+const TOOLS = {
+    select: new SelectTool("select"),
+    collect: new CollectTool("collect"),
+    delete: new DeleteTool("delete"),
+};
+
+const CONTEXT = {
+    node: "node",
+    link: "link",
+    mixed: "mixed",
+};
+
+class State extends Tool {
+    constructor() {
+        super("State");
+
+        this.display = new Display(TOOLS);
+
+        this.tool = undefined;
+        this.setTool(TOOLS.select);
+
+        // Shared variables
+        this.selectedItem = undefined;
+        this.selectedItems = new Set();
+        this.itemsContext = undefined;
+
+        this.keyStates = {};
+    }
+
+    setTool(tool) {
+        if (this.previousTool === tool) {
+            return;
+        }
+
+        this.previousTool = this.tool;
+        this.tool = tool;
+        this.display.setSelectedTool(tool);
+    }
+
+    setSelectedItem(item) {
+        this.selectedItem = item;
+        this.display.setSelectedItem(item);
+    }
+
+    addSelectedItem(item) {
+        this.selectedItems.add(item);
+        this.display.setSelectedItems(this.selectedItems, this.itemsContext);
+    }
+
+    removeSelectedItem(item) {
+        this.selectedItems.delete(item);
+        this.display.setSelectedItems(this.selectedItems, this.itemsContext);
+    }
+
+    clearSelectedItems() {
+        this.selectedItems.clear();
+        this.itemsContext = undefined;
+        this.display.setSelectedItems(this.selectedItems, this.itemsContext);
+    }
+
+    onNodeClick(node) {
+        this.tool.onNodeClick(node);
+    }
+
+    onLinkClick(link) {
+        this.tool.onLinkClick(link);
+    }
+
+    onKeyDown(key) {
+        var id = this.getKeyId(key);
+        var previous = this.keyStates[id];
+
+        this.keyStates[id] = true;
+
+        if (previous !== true) {
+            this.tool.onKeyDown(key);
+        }
+    }
+
+    onKeyUp(key) {
+        var id = this.getKeyId(key);
+        var previous = this.keyStates[id];
+
+        this.keyStates[id] = false;
+
+        if (previous !== false) {
+            this.tool.onKeyUp(key);
+        }
+    }
+
+    getKeyId(key) {
+        return key.keyCode;
+    }
+
+    nodeCanvasObject(node, ctx) {
+        var toolValue = this.tool.nodeCanvasObject(node, ctx);
+
+        if (toolValue !== undefined) {
+            return toolValue;
+        }
+
+        // TODO: Clean up function
+
+        // add ring just for highlighted nodes
+        if (this.selectedItem === node || this.selectedItems.has(node)) {
+            ctx.beginPath();
+            ctx.arc(node.x, node.y, 5 * 1.4, 0, 2 * Math.PI, false);
+            ctx.fillStyle = this.selectedItem === node ? "red" : "green";
+            ctx.fill();
+        }
+
+        // Draw image
+        if (node[NODE_IMAGE] !== undefined) {
+            var path = IMAGE_SRC + node[NODE_IMAGE];
+            var img = new Image();
+            img.src = path;
+
+            ctx.drawImage(
+                img,
+                node.x - IMAGE_SIZE / 2,
+                node.y - IMAGE_SIZE / 2,
+                IMAGE_SIZE,
+                IMAGE_SIZE
+            );
+        }
+
+        // TODO: Render label as always visible
+    }
+
+    nodePointerAreaPaint(node, color, ctx) {
+        var toolValue = this.tool.nodePointerAreaPaint(node, color, ctx);
+
+        if (toolValue !== undefined) {
+            return toolValue;
+        }
+
+        ctx.fillStyle = color;
+        ctx.fillRect(
+            node.x - IMAGE_SIZE / 2,
+            node.y - IMAGE_SIZE / 2,
+            IMAGE_SIZE,
+            IMAGE_SIZE
+        ); // draw square as pointer trap
+    }
+
+    nodeCanvasObjectMode(node) {
+        var toolValue = this.tool.nodeCanvasObjectMode(node);
+
+        if (toolValue !== undefined) {
+            return toolValue;
+        }
+
+        return "after";
+    }
+
+    linkWidth(link) {
+        var toolValue = this.tool.linkWidth(link);
+
+        if (toolValue !== undefined) {
+            return toolValue;
+        }
+
+        return this.isLinkHighlighted(link) ? 5 : 1;
+    }
+
+    linkDirectionalParticles() {
+        var toolValue = this.tool.linkDirectionalParticles();
+
+        if (toolValue !== undefined) {
+            return toolValue;
+        }
+
+        return 4;
+    }
+
+    linkDirectionalParticleWidth(link) {
+        var toolValue = this.tool.linkDirectionalParticleWidth(link);
+
+        if (toolValue !== undefined) {
+            return toolValue;
+        }
+
+        return this.isLinkHighlighted(link) ? LINK_PARTICLE_COUNT : 0;
+    }
+
+    redraw() {
+        this.display.setSelectedTool(this.tool);
+        this.display.setSelectedItem(this.selectedItem);
+        this.display.setSelectedItems(this.selectedItems, this.itemsContext);
+    }
+
+    isLinkHighlighted(link) {
+        return (
+            this.selectedItem === link ||
+            graph.isLinkOnNode(link, state.selectedItem)
+        );
+    }
+}
diff --git a/editor/js/tools/collecttool.js b/editor/js/tools/collecttool.js
new file mode 100644
index 0000000000000000000000000000000000000000..5554a0fff8683b2728ed1fcf3868cbfea2e04ef4
--- /dev/null
+++ b/editor/js/tools/collecttool.js
@@ -0,0 +1,37 @@
+class CollectTool extends Tool {
+    constructor(key) {
+        super("Collect", key);
+    }
+
+    onNodeClick(node) {
+        if (state.itemsContext !== CONTEXT.node) {
+            state.clearSelectedItems();
+            state.itemsContext = CONTEXT.node;
+        }
+
+        if (state.selectedItems.has(node)) {
+            state.removeSelectedItem(node);
+        } else {
+            state.addSelectedItem(node);
+        }
+    }
+
+    onLinkClick(link) {
+        if (state.itemsContext !== CONTEXT.link) {
+            state.clearSelectedItems();
+            state.itemsContext = CONTEXT.link;
+        }
+
+        if (state.selectedItems.has(link)) {
+            state.removeSelectedItem(link);
+        } else {
+            state.addSelectedItem(link);
+        }
+    }
+
+    onKeyUp(key) {
+        if (key.keyCode === 17) {
+            state.setTool(state.previousTool);
+        }
+    }
+}
\ No newline at end of file
diff --git a/editor/js/tools/deletetool.js b/editor/js/tools/deletetool.js
new file mode 100644
index 0000000000000000000000000000000000000000..6f990000c62e00d626fe4be61ef8e9566d6625df
--- /dev/null
+++ b/editor/js/tools/deletetool.js
@@ -0,0 +1,13 @@
+class DeleteTool extends Tool {
+    constructor(key) {
+        super("Delete", key);
+    }
+
+    onNodeClick(node) {
+        graph.deleteNode(node[NODE_ID]);
+    }
+
+    onLinkClick(link) {
+        graph.deleteLink(link[LINK_SOURCE][NODE_ID], link[LINK_TARGET][NODE_ID]);
+    }
+}
\ No newline at end of file
diff --git a/editor/js/tools/selecttool.js b/editor/js/tools/selecttool.js
new file mode 100644
index 0000000000000000000000000000000000000000..4c4b42742ae6e8487306d35cb1eefecae1003194
--- /dev/null
+++ b/editor/js/tools/selecttool.js
@@ -0,0 +1,19 @@
+class SelectTool extends Tool {
+    constructor(key) {
+        super("Select", key);
+    }
+
+    onNodeClick(node) {
+        state.setSelectedItem(node);
+    }
+
+    onLinkClick(link) {
+        state.setSelectedItem(link);
+    }
+
+    onKeyDown(key) {
+        if (key.keyCode === 17) {
+            state.setTool(TOOLS.collect);
+        }
+    }
+}
\ No newline at end of file
diff --git a/editor/js/tools/tool.js b/editor/js/tools/tool.js
new file mode 100644
index 0000000000000000000000000000000000000000..1b58655ce3ca4beb03f107b7312a5b24825f12e7
--- /dev/null
+++ b/editor/js/tools/tool.js
@@ -0,0 +1,77 @@
+class Tool {
+    constructor(name, key) {
+        this.name = name;
+        this.key = key;
+        this.warnings = false;
+    }
+
+    getName() {
+        return this.name;
+    }
+
+    getKey() {
+        return this.key;
+    }
+
+    onNodeClick(node) {
+        if (this.warnings) {
+            console.warn('Method "onNodeClick" not implemented.');
+        }
+    }
+
+    onLinkClick(link) {
+        if (this.warnings) {
+            console.warn('Method "onLinkClick" not implemented.');
+        }
+    }
+
+    onKeyDown(key) {
+        if (this.warnings) {
+            console.warn('Method "onKeyDown" not implemented.');
+        }
+    }
+
+    onKeyUp(key) {
+        if (this.warnings) {
+            console.warn('Method "onKeyUp" not implemented.');
+        }
+    }
+
+    nodeCanvasObject(node, ctx) {
+        if (this.warnings) {
+            console.warn('Method "nodeCanvasObject" not implemented.');
+        }
+    }
+
+    nodeCanvasObjectMode(node) {
+        if (this.warnings) {
+            console.warn('Method "nodeCanvasObjectMode" not implemented.');
+        }
+    }
+
+    nodePointerAreaPaint(node, color, ctx) {
+        if (this.warnings) {
+            console.warn('Method "nodePointerAreaPaint" not implemented.');
+        }
+    }
+
+    linkWidth(link) {
+        if (this.warnings) {
+            console.warn('Method "linkWidth" not implemented.');
+        }
+    }
+
+    linkDirectionalParticles() {
+        if (this.warnings) {
+            console.warn('Method "linkDirectionalParticles" not implemented.');
+        }
+    }
+
+    linkDirectionalParticleWidth(link) {
+        if (this.warnings) {
+            console.warn(
+                'Method "linkDirectionalParticleWidth" not implemented.'
+            );
+        }
+    }
+}
diff --git a/knowledge-space.php b/knowledge-space.php
index b03271b7dd01d9c0f8afc1175a4a788e4eeba63f..c9f5abddfec9eaa94051c5d975d75a21688d93ab 100644
--- a/knowledge-space.php
+++ b/knowledge-space.php
@@ -25,6 +25,20 @@ function ks_add_graph(): string
     return $three . $renderer .$renderer2 . $graph . $div . $variables . $script;
 }
 
+function ks_add_editor(): string
+{
+    // Proper, secure script loading in the future
+    // Reference https://stackoverflow.com/a/16823761/7376120
+    wp_enqueue_script('jquery');
+
+    $plugin_url = plugin_dir_url(__FILE__);
+
+    $raw_html = file_get_contents(__DIR__.DIRECTORY_SEPARATOR."editor".DIRECTORY_SEPARATOR."editor.html");
+    $ready_html = str_replace("%WWW%", $plugin_url, $raw_html);
+
+    return $ready_html;
+}
+
 function kg_load_css() {
     $plugin_dir = plugin_dir_url(__FILE__);
     wp_enqueue_style('kg-style', $plugin_dir.'kg-style.css');
@@ -32,3 +46,4 @@ function kg_load_css() {
 
 add_action('wp_enqueue_scripts', 'kg_load_css');
 add_shortcode('knowledge-space', 'ks_add_graph');
+add_shortcode('knowledge-space-editor', 'ks_add_editor');