Guide - Creating a Trusted Content Template Answer Generator

Overview

In this guide, we're going to show you how we created the Seat Geek Event Search Answer Generator, which shows upcoming events for a performer. The Answer Generator outputs inline blocks of content that use Javascript inside of an iframe.

Why do this in first place? Why not just let the user go to Seat Geek and find the events himself?

Here's a few reasons:
  • The user might not be searching for events directly, but might find upcoming events for a searched performer interesting
  • Navigating to another site requires the user to find the right source and click on it. Opening another site is especially painful on mobile devices because most devices require another window to be opened. If the user just wants a small bit of information, this is too much overhead.
  • The target site might not be optimized for mobile devices, or you might want show a custom presentation of the data other than what is on the web site

Of course, a Seat Geek event finder is just an example. Generally, you can use this guide to produce inline blocks of content that can execute Javascript.


Preliminaries

Before we dive in, let's make sure we aren't wasting our time and that what we want to do is feasible. First let's check if an Answer Generator for Seat Geek is available. Go to Solve for All's plugin search page and enter "seat geek". Select Seat Geek, and you will be taken to its detail page. In the detail page, go to the Try it out section and enter richard marx. You should be taken to the Solve for All answers page that shows upcoming events for Richard Marx, or a message stating that there are no upcoming events. At this point, you would probably just add this Answer Generator instead of creating your own (unless this Answer Generator didn't do everything that you wanted). But this Answer Generator was the result of creating this guide, so let's continue!

It would be nice if our Answer Generator could activate only when a performer's name is present in the answer query. To see if this is possible, let's see what happens when we search for a individual artist; try searching for ?debug richard marx. The Debug Answer Generator shows the recognition results which look like this:

{
  "org.dbpedia.ontology.MusicalArtist": [
    {
      "recognitionLevel": 1.7564186,
      "localRecognitionLevel": 1.7564186,
      "humanFormattedWikipediaArticle": "Richard Marx",
      "wikipediaArticleName": "Richard_Marx",
      "matchedText": "Richard Marx"
    }
  ],
  ...
}

Now let's try searching for a band: ?debug roxette. You should see something like this:

{
  ...,
  "org.dbpedia.ontology.Band": [
    {
      "recognitionLevel": 1.6985672,
      "localRecognitionLevel": 1.6985672,
      "humanFormattedWikipediaArticle": "Roxette",
      "wikipediaArticleName": "Roxette",
      "matchedText": "Roxette"
    }
  ],
  ...
}

So it looks like the Knowledge Base can already recognize artists and bands in the answer query, and it outputs recognition results with the recognition keys org.dbpedia.ontology.MusicalArtist and org.dbpedia.ontology.Band when it does.

Finally, let's see if it's possible to grab data from Seat Geek. Seat Geek brilliantly exposes an API that doesn't require authentication and can be called from any website (supports cross-origin requests). Using the API we can get information about performers as well as events. So it looks like we have all we need to make this Answer Generator work. If an API key or other authentication was required, we would have to use the Web Request API on the server side to avoid leaking credentials, but that might delay the display of search results on the client. We are going to output some HTML that can call the SeatGeek API from the client's browser, that way we can start showing the other results without waiting for the API response.


Creating an Trusted Content Template Answer Generator

Preparing the files

You can create an Answer Generator in a new project directory, but if you use the example plugins project, you'll gain the ability to write your scripts in ES6 and use Sass to write CSS. You can also check your template to ensure it is valid EJS. So let's clone the repo:

git clone https://github.com/jtsay362/solveforall-example-plugins

Instead of writing each file from scratch, we'll take a look at the existing files and see how they work together. Open src/answer_generators/seatgeek/seatgeek.html, in an editor. This is going to be the starting point for the template, but it uses a hardcoded value for the performer (roxette). Let's take a look at the file:

<html>
  <head>
    <!-- 1 -->
    <link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.6/spacelab/bootstrap.min.css"
     rel="stylesheet">

    <!-- 2 -->
    <link href="../../../build/answer_generator_content/seatgeek/styles/events.css"
     rel="stylesheet">
  </head>
  <body>
    <div id="performer_info"></div>
    <div id="events"></div>
    <div id="debug"></div>

    <!-- 3 -->
    <div id="data_to_transfer" data-performer="roxette"></div>

    <!-- 4 -->
    <script src="https://cdn.jsdelivr.net/g/jquery@1.11.3,underscorejs@1.8.3,bootstrap@3.3.6,momentjs@2.11.2">
    </script>

    <!-- 5 -->
    <script src="https://cdn.rawgit.com/jtsay362/ejs-no-node/1e39638e5a87ee940e94a3648ab00ed5d643c3b6/dist/ejs-no-node.min.js">
    </script>

    <!-- 6 -->
    <script src="../../../build/answer_generator_content/seatgeek/debug_scripts/seatgeek.js">
    </script>
  </body>
</html>

Here's what's going on:

  1. The CSS for Bootswatch Spacelab is included, which matches the style of Solve for All. Trusted content does not inherit the style of Solve for All, so you'll most likely need this to keep your content from clashing.
  2. This is the CSS that we are going to generate from the Sass file src/answer_generators/styles/events.scss. Our build tools will write them on the local filesystem, but we'll change this once the CSS file is available on Dropbox.
  3. Here we have a placeholder element that contains the performer name in a data attribute. The script we write will pull the performer name from here. When we convert this html file to an EJS template, we will replace roxette with the actual performer's name.
  4. We use jsdelivr to get multiple scripts in a single request. Note that jsdelivr is one of the trusted sources in the Chrome extension, so this Answer Generator should work there too.
  5. We include a version of EJS for client-side processing that works on browsers. We could use a different template library or no template library at all, but we use EJS since we are already using it on the server-side.
  6. Finally, we include the script that is going to call the SeatGeek API and update the DOM with the results. Our build tools will write this in the local filesystem, but we'll change this once the JS file is available on Dropbox.

If you are creating a new Answer Generator instead of just following along, you will need to add a few lines to gulpfile.js to tell it which scripts to process:

var ANSWER_GENERATOR_MODULES = {
  ...
  // BEGIN ADDED LINES
  ,
  seatgeek: {
    scripts: ['events']
  }
  // END ADDED LINES
};

In out case, we need only a single script, events.js, but if you had multiple scripts you would add them in the order required. Now let's process the files for debugging:

# Only needed once to install the NPM modules
npm install

# creates build/answer_generator_content/seatgeek/styles/seatgeek.css
gulp sass

# creates build/answer_generator_content/seatgeek/debug_scripts/seatgeek.js
gulp debug-process-content-scripts

# Optional: watches the scss and js files for any changes, and reloads the browser
gulp watch-content-scripts

All the files we need for debugging should exist now. We can try out this mini-web app by going to the project root (in another terminal window if you ran gulp watch-content-scripts) and typing:

python -m SimpleHTTPServer 8000

In your browser, navigate to http://localhost:8000/src/answer_generators/seatgeek/events.html You should see a photo of the band Roxette as well as upcoming events.

We're not going to cover the contents of seatgeek.js but basically, it's just a couple of AJAX calls to get the information about Roxette and the events, and populating the DOM with the presentation of the data. The code is written in ES6, so it requires transpiling by the gulp tasks. If you tweak it, and you ran gulp watch-content-scripts, it will update the generated debug version of seatgeek.js, and update the browser if you have connected it to LiveReload.

Once we are satisfied with the styles and script, we process them for production (minifying them):

# creates build/answer_generator_content/seatgeek/styles/seatgeek.css
gulp sass

# creates build/answer_generator_content/seatgeek/debug_scripts/seatgeek.js
gulp content-scripts

and upload them to Dropbox (RawGit is another option if you want to check the processed files into a GitHub repository). If you use Dropbox, make sure you place the files somewhere in your Public directory. For each file, right click it and choose the option Copy public link .... Substitute those URLs in the EJS file below.

The EJS file events.html.ejs is the same as events.html but with a few tweaks:

<%
  let performer = q;
  let relevance = 0;
  if (recognitionResult) {
    const articleName = recognitionResult.wikipediaArticleName;
    const article = _(recognitionResults['com.solveforall.recognition.WikipediaArticle']).find(a => {
      return (a.article === articleName);
    });

    // Should be lower than the Wikipedia result itself
    relevance = article.recognitionLevel - 0.01;
    performer = article.title;
  }
%>
<html>
  <head>
    <link href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.6/spacelab/bootstrap.min.css"
     rel="stylesheet">

    <link href="https://dl.dropboxusercontent.com/u/34577952/solveforall/plugins/seatgeek/events.css"
     rel="stylesheet">

    <meta name="com.solveforall.meta.answer.uri"
     content="https://seatgeek.com/search?f=1&search=<%= encodeURIComponent(performer) %>" >
    <meta name="com.solveforall.meta.answer.relevance"
     content="<%= relevance %>" >
  </head>
  <body>
    <div id="performer_info"></div>
    <div id="events"></div>
    <div id="debug"></div>
    <div id="data_to_transfer" data-performer="<%= performer %>"></div>

    <script src="https://cdn.jsdelivr.net/g/jquery@1.11.3,underscorejs@1.8.3,bootstrap@3.3.6,momentjs@2.11.2">
    </script>

    <script src="https://cdn.rawgit.com/jtsay362/ejs-no-node/1e39638e5a87ee940e94a3648ab00ed5d643c3b6/dist/ejs-no-node.min.js">
    </script>

    <script src="https://dl.dropboxusercontent.com/u/34577952/solveforall/plugins/seatgeek/seatgeek.js">
    </script>
  </body>
</html>

This is an EJS template, pretty similar to ERB, JSP, or ASP if you're familiar with those. Things to note in the template:

  • <% ... %> are code blocks
  • <%= ... %> blocks output the inner Javascript expression, which is HTML escaped
  • recognitionResult is set to the first matching recognition result by Solve for All. It corresponds to the first result mapped to one of the recognition keys we will set when we fill in the form to create the Answer Generator. In case the article name in Wikipedia has a suffix like (Band), we look up the title of the article from the Wikipedia recognition result that corresponds to this recognition result.

    If no recognition result is found, recognitionResult will be null, and in the template we fall back to using the entire query (q) as the performer name. This is useful if the Answer Generator is activated with an activation code or an activation keyword, but nothing in the query matches a known performer.

  • We use <meta> tags to set the URL and relevance for this answer
  • We have replaced roxette with the performer variable
  • All references to local files have been replaced with their locations in the public web (Dropbox in this example)

If you have Apache Ant installed, you can check that this is valid EJS. In the project root directory, enter the following command:

ant

If ant succeeds without reporting errors, that means the .ejs file is valid. To upload it to Solve for All, go to the Plugins page. If it's not selected already, select the Answer Generators tab. At the bottom of the table, select the button Create an Answer Generator. You should be taken to a long form which looks quite intimidating. Don't worry, we're going to fill it in step by step below.

Name: SeatGeek Test 1

This is whatever you want to use as the name of the Answer Generator, and will be shown to users when they select Answer Generators to add. Fill in SeatGeek Test 1. The reason for the Test 1 suffix is that Answer Generators must have unique names. Maybe somebody will already use this name so it's best to substitute a random number for 1.

Icon URL: https://seatgeek.com/favicon.ico

This is the favicon for Seat Geek. Nice that it is https.

Removed Query Terms: seatgeek,tickets,ticket,events,event,for,to,see

We want to convert phrases like seatgeek taylor swift, taylor swift events, and tickets to see taylor swift to taylor swift, so we'll ignore the terms seatgeek, tickets, ticket, events, event, for, to, and see, and concatenate them all together, separating them with a commas.

Removed Query Term Boosts: 1,0.3,0.3,0.2,0.2,0

If the user uses the terms seatgeek, events, tickets, ticket, fontawesome, it's very likely he wants to see the SeatGeek event list. So we'll boost the relevance by varying positive amounts, depending on the term. For seatgeek, the answer is almost certainly what the user wants, so we'll boost by 1. For events, event, tickets, and ticket, since SeatGeek isn't the only provider of tickets, we'll boost the relevance by a modest amounts (0.2 ~ 0.3). The rest of the terms shouldn't affect the relevance, and we won't boost their relevance at all. Now we take the value of the Removed Query Terms, seatgeek,tickets,ticket,events,event,for,to,see, and for each term, substitute the desired boost, to get 1,0.3,0.3,0.2,0.2,0,0,0. We can omit trailing boosts that are the same value as the previous one, so finally we get 1,0.3,0.3,0.2,0.2,0.

Provider: SeatGeek

This is optional, but we can find SeatGeek in the table of Content Providers, so let's use it. You can also create your own Content Providers on the Plugins page.

Required Content Recognizers: (Leave unset)

We don't need to detect any patterns in the query since the we're using recognition results from the Knowledge Base which is always searched.

Searched Semantic Data Collections: (Leave unset)

Again, since the we're using recognition results from the Knowledge Base, we don't need to search any Semantic Data Collections.

Categories: Music

Hit the Set Categories button. A popup should appear letting you choose one or more categories. The category that makes sense for performances is Music, so we'll pick that. Search for it using the search form in the top right corner, and check it. When done, hit the Set Categories button.

Type: Content Template

Since we're outputting inline content from a template, we'll set the type of Answer Generator to Content Template.

Recognition Keys: org.dbpedia.ontology.Band,org.dbpedia.ontology.MusicalArtist,org.purl.dc.AmericanRecordingArtist,org.purl.dc.BritishSingersongwriter

I went ahead and took the recognition keys we discovered previously, and added a couple more.

Label: SeatGeek

This is the label of the link that will be generated.

Tooltip: Search for

A tooltip for the answer isn't needed, and may be distracting, so let's leave the tooltip blank.

Relevance: (Leave blank)

If this is left blank, the answer produced by this Answer Generator will have a relevance equal to the recognition level of a matching document, plus the Removed Query Term Boosts if a Removed Query Term is found. This is what we want, so we'll leave the relevance blank.

Max Results: 1

We only want a single inline block from this Answer Generator, so we'll leave this at 1.

Content Template File: (Select events.html.ejs)

Attach the EJS file we just created.

Server Side Sanitized: (Uncheck)

We need to run Javascript inside an iframe, so we need to ensure this is checked. The downside is that the user will have to grant the trusted_content permission before the answer is shown.

Minimum Width, Preferred Width, Minimum Height: (Leave blank)

The defaults for these settings are fine, so we'll leave these blank.

Preferred Height: 250

By trial and error, a height of 250 pixels seems to be optimal considering the cases in which there are events found and when there aren't.

Requires Recognition Results: (Leave checked)

Since we need the results of searching Knowledge Base before this Answer Generator runs, we need to leave this checked.

Default Activation Code: sgtest1

We can pick a default activation code that will let any user activate this Answer Generator even if she has not installed it yet. Activation codes have to be unique, so if another Answer Generator has the same activation code, we'll get an error when we submit the form. If you'll following along this guide, you'll probably want to use a different suffix to avoid uniqueness errors.

Also, keep in mind that activation codes less than 5 characters have to be approved by Solve for All before they become active.

Required Permissions: trusted_content

This Answer Generator requires the user to grant the trusted_content permission (since it isn't server-side sanitized), or else it won't be included.

Optional Permissions: (Leave blank)

We don't need any optional permissions right now. In the future, we could use the user's the location when we search SeatGeek, and then we would need to specify a location permission here.

Publicly Visible, Publicly Linkable: (Check)

If these are checked, this Answer Generator will be usable by all users. Otherwise, only you can use it. Since we want to share our knowledge, we'll leave these checked.

Associate with Any Trigger: (Leave unchecked)

If the query isn't found in the Knowledge Base, it still could be a real performer, because the Knowledge Base isn't 100% complete (nor will it ever be). SeatGeek can still search for a performer though so we'll associate the Answer Generator with the Any Trigger to give the user a choice.

Default Descriptor - Name: (Leave blank)

These Default Descriptor properties are fallbacks for localized properties in case the specialized properties don't exist for the user's locale. By default, the name of the Default Descriptor is filled in with the Name property above, so we can leave this blank.

Default Descriptor - Description: Show SeatGeek events

Fill this field in with a good description of this Answer Generator; we'll use Show SeatGeek events.

Default Descriptor - Catalog Small Image: (Downloaded copy of https://seatgeek.com/favicon.ico)

The favicon for SeatGeek is quite big, so we'll download it and attach it here.

Save

Now that we are done filling in all the fields, we'll hit the Save button. Now the Answer Generator is saved and ready to go!

Testing the Answer Generator

Let's test it out be activating the Answer Generator with its activation code. Assuming you picked sgtest1 as the activation code, try searching for ?sgtest1 roxette. You should see a photo of Roxette and upcoming events in a content block. Nice!

At this point, any user can choose to activate the Answer Generator either with the activation code ?sgtest1 or by choosing customized keywords (defaulting to seatgeek, events, event, tickets, and ticket). To see this, let's find the Answer Generator in the customization section. Go to the Find Plugins page and search for your Answer Generator. Then go to its detail page, and in the Your Configuration section, hit the Add button. On the first step, select an Engine that is active by default. On the next step, notice that seatgeek, events, event, and event are the default activation keywords. Try using the default set of activation keywords, and saving this configuration. Then search for seatgeek adele. You should get a block of content with a photo of Adele and upcoming events. (If not, check that the Engine you selected was active by default.) Congratulations, you've just created an Answer Generator for anyone to use!


(Optional) Creating a Trigger

Our full goal is to have this Answer Generator could produce a block of content whenever the name of a performer is in the query. We already know the recognition keys to look for when a performer is detected.

So let's create a Trigger that implements the rule,

If there is a recognition result mapped to any of the keys org.dbpedia.ontology.Band,
org.dbpedia.ontology.MusicalArtist, org.purl.dc.AmericanRecordingArtist, or
org.purl.dc.BritishSingersongwriter, and its recognition level is above a
certain threshold, activate the Answer Generator.

To do this, we need to create a Simple Trigger. Go to the Plugins page and select the Triggers tab. Then hit the Create a Trigger button after the table. You'll be taken to a form to create a trigger. Let's start filling it in.

Name: Musical Performer Test 1

This is whatever you want to use as the name of the Trigger, and will be shown to users when they select Triggers to activate their Answer Generators. Fill in Musical Performer Test 1. The reason for the Test 1 suffix is that Triggers must have unique names. Maybe somebody will already use this name so it's best to substitute a random number for 1.

Required Content Recognizers: (Leave as is)

Again, we don't need to detect any patterns in the query, so no content recognition is needed and we'll leave this unset.

Required Semantic Data Collections: None

Again, since the we're using recognition results from the Knowledge Base, we don't need to search any Semantic Data Collections.

Documentation file: (Leave unset)

Once we are sure the Trigger works as we expect, we could upload a file describing how it works here, but for now we can leave this unset.

Type: Simple

Simple Triggers implement the simple rule: if the recognition level mapped to a recognition key is greater than some threshold, activate the Answer Generator. That's exactly what we want, so let's pick Simple.

Recognition Keys: org.dbpedia.ontology.Band,org.dbpedia.ontology.MusicalArtist,org.purl.dc.AmericanRecordingArtist,org.purl.dc.BritishSingersongwriter

These are the recognition keys that I found mapped to musical performers.

Min Recognition Level: 0.9

When we tried ?debug richard marx and ?debug roxette, we got recognition levels close to 1.7. But normally recognition levels should be below 1.0, and so we are going to use 0.9. We can come back and adjust this if we get too many false positives or false negatives.

Publicly Visible, Publicly Linkable: (Check)

If these are checked, the Trigger will be usable by all users. Otherwise, only you can use it. Since we want to share our knowledge, we'll leave these checked.

Default Descriptor - Name: (Leave blank)

By default, the name of the Default Descriptor is filled in with the Name property above, so we can leave this blank.

Default Descriptor - Description: Activates when a musical performer is found

Fill this field in with a good description of the Trigger..

Save

Now we are done filling in all the fields, we'll hit the Save button. Now the Trigger is saved and ready to go! That was easy.


(Required if you created a Trigger) Creating an Answer Generator - Trigger Association

Before the user can use this Trigger with the Answer Generator we created, we need to tell Solve for All that they can be used together. To do that, we'll need to create an Answer Generator - Trigger Association. Don't worry -- it's short and sweet. Go to the Plugins page and click the Associations tab. Find the button Create an Association and click it. You will be taken to a short form to create an Association. Let's fill it in.

Answer Generator: SeatGeek Test 1

Hit the Set Answer Generator button and find the Answer Generator you just created. When done, hit the Set Answer Generator button.

Trigger: Musical Performer Test 1

Hit the Set Trigger button and find the Trigger you just created. When done, hit the Set Trigger button.

Publicly Visible, Publicly Linkable: (Check)

If these are checked, this Association will be usable by all users. Otherwise, only you can use it. Since we want to share our knowledge, we'll leave these checked.

Save

Now we are done filling in all the fields, we'll hit the Save button. Now the Association is available to all users. That was even easier than creating a Trigger.

Try it out

Let's add this association and try it out. Find the Answer Generator in the customization section again. Go to the Find Plugins page and search for your Answer Generator. Then go to its detail page, and in the Your Configuration section, hit the Add button. First select an Engine that is active by default. On the next step, you can configure how you want to activate this Answer Generator. Notice that your new Trigger is listed. Select it, save your settings, and do a search for kenny g. You should see the SeatGeek inline answer show up. If not, check that the Engine you selected was active by default.

If everything is working, you have successfully associated your Answer Generator with an activation Trigger that makes sense and all other users can do the same. If you share the Engine that contains your configured Answer Generator, any user that adds your Engine will automatically get this configuration.


Next Steps

Now take this example and tweak it for you own use case. We're looking forward to see what awesome stuff you come up with!

For advanced usage, you can check the documentation of Answer Generators and Inline Answers for more details.