Back to Software

Tapestry Tool

Tapestry is an educational concept-mapping tool that allows users to visually organize ideas and create connections between them. During my employment as a software engineer at the Emerging Media Lab, I was tasked with developing a system that integrates AI to automatically generate fully structured Tapestries. I will do my best to summarize all my work here, but if you want to learn more you can follow these links:

Our stakeholders wanted the following features added to their existing architecture:

Before

The Tapestry tool originally relied on a manual drag-and-drop workflow, where users had to position each node and its connections one by one.

The original manual Tapestry node positioning workflow

After

During my work term, I was asked to integrate generative AI into Tapestry's existing WordPress architecture and work with the Tapestry API to automatically build hierarchical and interconnected knowledge maps from content and prompts the user provides.

The automated AI-assisted Tapestry generation workflow

Key Definitions

Key definitions for the Tapestry system

The Hierarchy and Procedural Layout System

The first thing I worked on was figuring out how to convert an open-ended response from a generative model into something structured that could then be processed by the Tapestry API. This is what I came up with: When the user inputs a prompt, the system first creates a root node at the center of the user’s screen. Every node has a unique ID, and every node also stores the ID of its parent through a parentId. The root has no parent. For the first layer of children nodes, the model is prompted to return around five to six concepts related to the user input, each around 2-3 words long. It is important to note that this project was done in 2023 which was before the Structured Outputs feature was added, which could have helped keep the response to a JSON schema. At the time, I used prompts and character delimiters to make the response predictable enough for the application to process it. Once my code separated the model response into individual titles for each concept, I sent them to the Tapestry node endpoint with the ID of the root node as their parentId. The important part about this was making sure to keep track of the ID returned by Tapestry right after each new node was created:

const response = await axios.post(
  nodeGeneratingEndpoint,
  nodeData
);

childNodeIds.push(response.data.node.id);

I stored the returned IDs in an array in the same order as the generated concepts, which created a direct relationship between each concept title and its corresponding node. This meant I did not have to search through the complete Tapestry to identify a specific node later on. For the second request, I submitted the complete list of the first layer concepts together and asked the model to generate a group of more specific subtopics for each one. Instead of making a separate model request for each node in the first layer (which would have been easier but it would also cost more), the response contained the content for all branches of the hierarchy at once so each returned group was kept in the same indexed order as the first layer concepts. My code could then associate branch i with the tapestry node ID stored at position i. Each subtopic in that branch was then created with its corresponding first layer node ID as its parent Id.

for ( let branchIndex = 0; branchIndex < branches.length; branchIndex++ ) {
  const parentId = childNodeIds[branchIndex];

  for (const title of branches[branchIndex].subtopics) {
    await axios.post(nodeGeneratingEndpoint, {
      node: {
        title,
        status: "publish",
      },
      parentId,
    });
  }
}

This mapping is how I was able to turn the generative model’s response into an actual hierarchy. Another thing to mention is that in the second prompt, I made sure to include the user’s original topic along with the first layer concepts, since this was important for ensuring every branch topic generated is still related to the overall subject.

Ok so this hierarchy code worked really well when I tested it with small tapestries, like these ones:

Small generated Tapestry example 1 Small generated Tapestry example 2 Small generated Tapestry example 3 Small generated Tapestry example 4

However, when I tried generating a web, this happened…

Tapestry with incorrectly positioned nodes

The text populating the nodes were fine, but their positioning on the screen was completely off. Since different user prompts generated different hierarchies, I needed a way to position the nodes at runtime without knowing how many there would be in advance. I started by breaking the problem down geometrically to figure out how nodes could be distributed around their parents without relying on fixed positions.

Brainstorm board showing geometric node layout ideas
My brainstorm board in the office

I began by finding the coordinates of the root node by placing it in the center of the user’s viewport.

Root node coordinate calculation

Next, I needed to position the child nodes evenly around the root, so if there are n child nodes then I divide the circle (2π) into n equal sections. Then I calculate the actual x and y coordinates relative to the parent node, which is the root node. For the first layer, I made each node around 900 pixels away from the root.

First-layer child node coordinate calculation

I used the same general idea for the grandchildren nodes. Instead of arranging them around the root, I used the coordinates of each first layer node as the center of the smaller circle (x_c, y_c). For this layer, I used a smaller radius of 320 pixels. The main difference in this calculation is that if a parent has m grandchildren, I divided the circle by m+1 to leave some clearance for the link between the parent and the root node.

Grandchild node coordinate calculation

In my code, I don’t reset the cn_angle (cn_angle = child node angle, this was in relation to the first layer nodes, so “cn” in this naming meant the child of the first layer node which were the grandchildren…sorry for any confusion it's just much simpler to explain with all things relative to the root node) variable with each grandchild position calculation. This means the next grandchild (even if it belongs to a different child node) inherits the angle where the previous child finished. This generally allows less crowding around the root node and prevents overlap with the link connecting the first layer node to the root node.

Continuous grandchild angle calculation

With this code, all the node coordinates are calculated dynamically, allowing the layout to automatically adapt to any topic and different numbers of generated nodes.

Successfully generated dynamic Tapestry layout

Making the Web

At this point the Tapestry had a good hierarchy and layout on the screen, but it was still a tree. To create the full web, I needed to find connections between concepts from different branches and add links between them. I used a nested for loop to create an array of every unique pair of grandchild nodes.

for (let i = 0; i < nodes.length; i++) {
  for (let j = i + 1; j < nodes.length; j++) {
    pairs.push([nodes[i], nodes[j]]);
  }
}

This code makes sure that the node will not be compared with itself and the same pair is not compared twice (e.g. (B, A) and (A, B) would not both exist in the array). For m grandchild nodes, the total number of unique pairs would be [m(m-1)]/2 so even small Tapestries would need a lot of comparisons. I batched every pair into a single request where the model was asked to assign each pair a semantic relevance score between 0-100 where 0 means the concepts are unrelated while 100 means they are quite strongly related. I used a minimum relevance threshold of 70 to determine whether Tapestry should create a link between two nodes. For every pair that passed this threshold, I used their stored node IDs as the source and target of a new link.

Semantic connections used to create the Tapestry web

WordPress Plugin Integration

My initial generation system was built as a Python proof of concept. Once the core functionality worked, I had to integrate it into the existing Tapestry WordPress plugin so users could generate a Tapestry directly from the platform. The Tapestry frontend was built using Vue 2 and BootstrapVue, while Axios handled the HTTP requests and Vuex managed shared application state. The WordPress plugin also exposed the REST API endpoints used to create, update and connect Tapestry nodes. I refactored my Python code into a JavaScript module, and instead of creating a separate AI interface I integrated this directly into Tapestry’s existing architecture by reusing the platform’s existing REST endpoints and node objects. This basically means that the automatically generated nodes enter the same data model and rendering as manually created nodes, so users can generate an initial map then manually add more nodes and edit them. I used Vuex for things like deleting generated nodes and highlighting parts of the web that are connected. I also styled the new input and loading page so that this additional feature followed Tapestry’s original brand. This made my prototype into something people could actually use, which was especially important when we began user testing (more on this later).

Tapestry input interface wireframe
Tapestry WordPress plugin integration

PDF to Tapestry

To make the tool useful for existing learning material, I developed a feature that allows users to generate a Tapestry directly from a PDF document. My first approach attempted to extract the text inside of the Vue frontend using pdfjs-dist but I had a lot of trouble making the extraction reliable. I decided to move this extraction to a Python Flask backend using PyPDF2. The Vue frontend would take the file either through a drag and drop or file picker then put it in a FormData object so it can send it to the Flask endpoint using Axios. I made a Flask route that took this PDF, iterated through every page and extracted the text before combining it all into one string. It then returned the completed text back to the Vue frontend which passed it directly to the same function used for user prompts to generate a Tapestry.

PDF to Tapestry generation flow

User Testing

During development, we conducted two rounds of user research. The first round (June) focused on interviewing the participants and understanding how they responded and interacted with our initial prototype. This was our prototype in June:

Tapestry prototype used during the June testing round

After iterating on the design based on their feedback, we conducted a second round of testing in August which focused more directly on evaluating the usability of the revised prototype. This was our prototype in August:

Revised Tapestry prototype used during the August testing round
August Tapestry prototype interaction

All participants in both rounds completed an exit questionnaire which had questions from both the System Usability Scale (SUS) and USE. These scales measured different aspects of user experience:

  • SUS gave one overall usability score out of 100, and participants would answer each question using a scale from 1 to 5 (1 is strongly disagree and 5 is strongly agree). For example, “I would like to use this website frequently” or “I found the website unnecessarily complicated”, etc.
  • USE separated the experience into categories that were each scored out of 7 (usefulness, ease of use, ease of learning, and satisfaction). Note that in round 1 only usefulness was scored.

Here were the scores from our first round of testing:

Usefulness4.65/7
SUS67/100

Here were the scores from our second round of testing:

Usefulness5.64/7
Ease of Use5.71/7
Ease of Learning6.25/7
Satisfaction5.89/7
SUS84/100

Our overall SUS score increased by 17 points, while the usefulness of our features increased from 4.65/7 to 5.64/7. This shows that the changes I made between these testing rounds, including things like minimizing unselected nodes and highlighting selected nodes and links improved both the overall usability of the tool and how valuable users found the new features I added.

Conference Posters

The first poster was presented at the annual Emerging Media Lab Showcase. The second poster was presented at the 2024 Multidisciplinary Undergraduate Research Conference.

Tapestry project poster
View the Tapestry MURC poster PDF
Back to Top