In previous posts, we developed a small web application that runs in the cloud then we transformed it into an application that analyses given text and returns tone scoring per sentence. As we’ve been focusing on the functionality, the extended application was returning a rude output.

With most people judging a web app based on its look and feel, we’ll turn our bare application into something more presentable.

For this, we will use bootstrap as a CSS base and Perl HTML::Template to improve the overall readability of the code.

1. Build the Page Library

We’ll start by building a small library that based on some parameters passed in the environment generates a full type-set HTML page.

This is the basic description of what our content function does and what it looks like:

# function generating content
#
# Parameters passed via hash
#
# param : contains hash to tag-value pairs passed to HTML::Template
# path : path to template files (defaults to URI path excluding tmpl subdir)
# tmpl : name of tmpl file (defaults to URI name or index if none found)
# file : full path to tmpl file (overwrites path+tmpl)
# rc : html return code
#
sub content {
        my $env = shift;

        my ($header, $rc) = ($env->{header}, $env->{rc});

        $header ||= {'Content-type' => 'text/html'};
        # template
        my ($path, $tmpl) = $env->{REQUEST_URI} =~ /\/?(.*)\/([^\/]*)$/;
        $tmpl =~ s/[^a-z].*$//;
        $tmpl = $env->{tmpl} || $tmpl || 'index';
        $path ||= $env->{path} || '.'; 
        my $file = $env->{file} || "$path/tmpl/$tmpl.tmpl";
        my $template = HTML::Template->new(filename => $file, path => ['.', "$path/tmpl"], default_escape => 'html');

        $env->{param}->{tmpl} ||= $tmpl;
        $env->{param}->{title} ||= "$path $tmpl";
        $template->param(%{$env->{param}});
        $content = $template->output;

        # fallback defaults for type $env->{content}
        $rc ||= 200;

        # generate return
        my $res = Plack::Response->new($rc, $header, $content);
        return $res->finalize;
}

The content routine tries to be a smart version of the HTML::Template directory. By default, the routine should more or less provide default values that work but allow the main routine to overwrite these defaults where needed.

We start evaluating the entries in our $env and parse the requesting URL. If no template file is specified, the fall-back is the name specified in the URL. If none is present use ‘index’.

Next, we determine the path following a similar track as for the template file. Eventually, use the path and add the ‘.tmpl’ extension to get the template name.

The HTML::Template library, with some additional directories to search for template files, can be instructed to format the HTML content. This content can be returned after replacing all reference with the values that can be found in the parameter hash ‘$env→{parameter}’.

To complete the Page library, a number of additional functions are defined to pass additional messages to the page. To keep our file tree clutter-free, we create directories ‘lib’ and ‘tmpl’ that will contain our ‘Page.pm’ library and the HTML templates.

This is what Page.pm looks like:

package Page;

# set import methods
use Exporter 'import';
our @EXPORT_OK = qw(content param error);

use Plack::Response;
use HTML::Template;
use strict;

# function generating content
#
# Parameters passed via hash
# param : contains hash to tag-value pairs passed to HTML::Template
# path : path to template files (defaults to URI path excluding 'tmpl' subdir)
# tmpl : name of tmpl file (defaults to URI name or 'index' if none found)
# file : full path to tmpl file (overwrites path+tmpl)
# rc : html return code
#

sub content {
        my $env = shift;

        my ($header, $rc) = ($env->{header}, $env->{rc});

        $header ||= {'Content-type' => 'text/html'};
        # template
        my ($path, $tmpl) = $env->{REQUEST_URI} =~ /\/?(.*)\/([^\/]*)$/;
        $tmpl =~ s/[^a-z].*$//;
        $tmpl = $env->{tmpl} || $tmpl || 'index';
        $path ||= $env->{path} || '.';
        my $file = $env->{file} || "$path/tmpl/$tmpl.tmpl";
        my $template = HTML::Template->new(filename => $file, path => ['.', "$path/tmpl"], default_escape => 'html');

        $env->{param}->{tmpl} ||= $tmpl;
        $env->{param}->{title} ||= "$path $tmpl";
        $template->param(%{$env->{param}});
        my $content = $template->output;

        # fallback defaults for type $env->{content}
        $rc ||= 200;

        # generate return
        my $res = Plack::Response->new($rc, $header, $content);
        return $res->finalize;
}
sub alert {
        my ($env, $str, $type) = @_;

        $env->{param}->{alert} ||= [];
        push(@{$env->{param}->{alert}}, { msg => $str, type => $type });
}

sub error {
        alert(@_, 'danger');
}

sub warning {
        alert(@_, 'warning');
}

sub info {
        alert(@_, 'info');
}

sub success {
        alert(@_, 'success');
}

(2*2).'2';

2. Use Templates

The HTML template files will be stored in the tmpl subfolder. By default, we’ll use a header.tmpl file and a footer.tmpl file that will be included in all of our main templates.

They look something like this:

<!DOCTYPE html>
<html>
<head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
     <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
     <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
     <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script>
     <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script>
     <title>Tone assistant | <TMPL_VAR name="title"></title>
</head>
<body class="<TMPL_VAR name='section'> <TMPL_VAR name='title'> tmpl-<TMPL_VAR name='tmpl'>">

<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark fixed-top">
     <div class="container">
          <div class="collapse navbar-collapse" id="navbarResponsive">
               <ul class="navbar-nav ml-auto">
                    <li class="nav-item active">
                         <a class="nav-link" href="/">Hello world</a>
                    </li>
                    <li class="nav-item">
                         <a class="nav-link" href="/tone">Tone assistant</a>
                    </li>
               </ul>
           </div>
     </div>
</nav>
 

<!-- Footer -->

<footer class="py-5 bg-dark">

     <div class="container">

          <p class="m-0 text-center text-white">Copyright &copy; Nexperteam BV 2020</p>

          </div>

     <!-- /.container -->

</footer>
</body>
</html>

The use of base templates for header and footer allows us to alter the look and feel of the entire application without having to go through all pages.

The header page also sets the title and contains a reference to the template and the path used. This might be useful if you start changing CSS on a page by page basis. It has the added bonus of simplifying the page templates substantially.

Note that we are using Bootstrap and we specify this in the header file to all files and the header will automatically do the same. The header also includes a link to all our “sub” pages, the “Hello world” routine and the “Tone assistant” routine.

Let’s revisit our original “hello world” routine. The code can be simplified to:

sub main {
     my $env = shift;
     return Page::content({ text => "Hello World" });
}

with an index.tmpl file in the tmpl directory

<TMPL_INCLUDE name="header.tmpl">

<!-- Page Content -->

<div class="container">
     <header class="jumbotron my-4">
          <img src="style/nxp2.png" class="float-right w-25">
          <h1 class="display-5">Hello world</h1>
     </header>
</div>
<!-- /.container -->

<TMPL_INCLUDE name="footer.tmpl">

Pushing out the new app and visiting our application will provide us with a slick starting page.

3. Add some Tone colour

Next up, let’s revisit the ‘tone’ page.

With all our hard work on the header and the footer, the main template pages remain rather sober and ‘tone.tmpl’ looks something like this:

<TMPL_INCLUDE name="header.tmpl">

<script>$(function () { $('[data-toggle="tooltip"]').tooltip() })</script>

 

<!-- Page Content -->

<div class="container">

     <header class="jumbotron my-4">

          <img src="style/nxp2.png" class="float-right w-25">

<h1 class="display-5">Tone assistant</h1>

</header>

 

<TMPL_IF name="alert">

<TMPL_LOOP name="alert">

     <div class="container alert alert-<TMPL_VAR NAME='type'>" role="alert"><pre><TMPL_VAR name="msg"></pre></div>

</TMPL_LOOP>

</TMPL_IF>

 

     <div class="my-2">

          <TMPL_VAR name="tone" escape="none">

     </div>


     <div class="my-2">

          <form>

              <div class="form-group">

                   <label for="textinput">Provide your text</label>

                   <textarea class="form-control" id="textinput" rows="5" name="text"><TMPL_VAR name="text"></textarea>

              </div>

             <input class="btn btn-primary" type="submit" value="Analyze tone">

          </form>

     </div>

</div>
<!-- /.container -->

<TMPL_INCLUDE name="footer.tmpl">

The first line is a little JavaScript snippet that will activate tooltips, followed by a header section. In case our program would generate an alert message, these will be displayed next.

The bottom half of the template contains the “Tone” output and the form nicely formatted in bootstrap style.

4. Set Tone Vizualisation

Using the Page library, our tone subroutine turns into:

sub tone {

     my $env = shift;

     $env->{req} = Plack::Request->new($env);

      

     my $text = $env->{req}->param('text');

     if($env->{param}->{text} = $text) {

          my $result = $ua->post($api_url, 'Content-Type' => 'application/json', Content => encode_json({text => $env->{param}->{text}}));

          if($result->is_success) {

               $env->{param}->{tone} = beautify($text, decode_json $result->decoded_content);

          } else {

     Page::error($env, $result->status_line);

     }

}
return Page::content($env);

}

We track the visualization parameters in a hash that we create in the $env hash – a hash in a hash.

First, we set the ‘text’ entry. This is what “<TMPL_VAR name=”text”>” will be replaced with. In case we run into trouble we just pass the return code from the web call to our Page::error function that will take care of displaying the error in the “<TMPL_IF name=”alert”>…” block. If we have a result, we pass it to the “beautify” function.

This beatify function takes two arguments, the original text and the ‘Tone analyzer’ structured data. It will return an HTML formatted text block prepared to be displayed as-is.

our %tone = (
     anger => "text-danger",
     fear => "text-warning",
     joy => "text-success",
     sadness => "text-warning",
     analytical => "text-info",
     confident => "text-success",
     tentative => "text-primary",
);

sub beautify {
     my ($text, $tone) = @_;
     for my $sentence (@{$tone->{sentences_tone}}) {
          my ($class, $legend) = ('', '');
          for my $tone (@{$sentence->{tones}}) {
               $class .= " $tone->{tone_id} $tone{$tone->{tone_id}}";
               $legend .= " $tone->{tone_name}: $tone->{score}\n";
          }

          if($class) {
               $text =~ s/$sentence->{text}/<div class="$class" data-toggle="tooltip" title="$legend">$sentence->{text}<\/div>/;
          }
     }

     my $overallscore = '<table class="table"><thead><tr><th scope="col">Overall tone</th><th scope="col">Score</th></tr></thead><tbody><tr>'.
     join('</tr><tr>', map { "<td>$_->{tone_name}</td><td>$_->{score}</td>\n" } @{$tone->{document_tone}->{tones}}).
     '</tr></tbody></table>';

 
     return $text;
}

The beautify routine uses an external hash to convert the tone_id entries from the Tone Analyzer to a more bootstrap friendly class-type.

The routine itself runs overall ‘sentences_tone’ entries, determines the class list which contains the original tone_id and the alternative matching bootstrap class.

In the same loop, we also create a legend describing what the exact scoring entails. This will be visible as a tool-tip if you roll over a particular sentence which returned a score.

Per entry in the sentences_tone list, both variables are calculated and the described sentence in the text is replaced by the <div> with the appropriate classes and a tool-tip legend around the text.

To finalize, the routine calculates the over-all score and puts it in a table.

The function returns the concatenation of the over-all score and the HTML formatted text as a string to the calling routine.

The app.psgi file needs to include the ‘lib’ path to search for libraries such as our own created ‘Page’ library which we also have to include.
Since we are using some graphic elements we also created a ‘static’ path providing our app with a way forward to directly serve content that does not need any processing such as images, javascript files and CSS file. In this case, anything that ends in ‘.png’.

#! /usr/bin/perl

 

use Plack::Builder;

use Plack::Request;

use Plack::Response;

use Plack::Middleware::Static;

use LWP::UserAgent;

use JSON;

use strict;

 

use lib qw(lib);

use Page;

 

our $api_key = 'XXXXX';

our $api_ins = 'api.eu-de.tone-analyzer.watson.cloud.ibm.com/instances/XXXX';

our $api_url = "https://apikey:$api_key\@$api_ins/v3/tone?version=2017-09-21";

our $ua = LWP::UserAgent->new(timeout => 10);

 

     our %tone = (

          anger => "text-danger",
          fear => "text-warning",
          joy => "text-success",
          sadness => "text-warning",
          analytical => "text-info",
          confident => "text-success",
          tentative => "text-primary",
     );

sub beautify {
     my ($text, $tone) = @_;
     for my $sentence (@{$tone->{sentences_tone}}) {
          my ($class, $legend) = ('', '');
          for my $tone (@{$sentence->{tones}}) {
               $class .= " $tone->{tone_id} $tone{$tone->{tone_id}}";
               $legend .= " $tone->{tone_name}: $tone->{score}\n";
          }

          if($class) {
               $text =~ s/$sentence->{text}/<div class="$class" data-toggle="tooltip" title="$legend">$sentence->{text}<\/div>/;
          }
     }

     my $overallscore = '<table class="table"><thead><tr><th scope="col">Overall tone</th><th scope="col">Score</th></tr></thead><tbody><tr>'.
          join('</tr><tr>', map { "<td>$_->{tone_name}</td><td>$_->{score}</td>\n" } @{$tone->{document_tone}->{tones}}).
          '</tr></tbody></table>';
        
       return $overallscore.$text;

     }

sub tone {
     my $env = shift;
     $env->{req} = Plack::Request->new($env);
 
     my $text = $env->{req}->param('text');
     if($env->{param}->{text} = $text) {
          my $result = $ua->post($api_url, 'Content-Type' => 'application/json', Content => encode_json({text => $env->{param}->{text}}));
          if($result->is_success) {
               $env->{param}->{tone} = beautify($text, decode_json $result->decoded_content);
           } else {
               Page::error($env, $result->status_line);
               }
     }
      return Page::content($env);

}

sub main {
     my $env = shift;
     return Page::content({ text => "Hello World" });
}
 
our $app = builder {
     enable 'Plack::Middleware::Static', path => qr{.*\.png}, root => '.';
     mount "/tone" => \&tone;
     mount "/" => \&main;
}

Conclusion

Providing a nice and slick web interface does not have to be a huge effort if you are happy with a default CSS framework such as bootstrap.

For a modest proof-of-concept application, it provides a clean and easy way to be integrated in a solution. Combining such a layout system with a template-driven library can clean up an application in a matter of minutes making it into something presentable.

In future posts, we’ll talk about the app’s database and security.

 

Drop me a note below with your thoughts and questions about building cloud applications and I’ll do my best to answer them in future posts.