# Project export: No Scam, Only Ham

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2024
- Tagline: Scamming is a prevalant issue, especially for the elderly. We created a scam detection package to help reduce the likelihood of getting scammed with a fun quiz as well as a NLP scam detection model.
- Devpost: https://devpost.com/software/no-scam-only-ham
- GitHub: https://github.com/jenni-mori1/noScamOnlyHam.git
- Demo: https://www.canva.com/design/DAF9GcG9Nag/D3is8b35w816SQmnVQJoGA/edit?utm_content=DAF9GcG9Nag&utm_campaign=designshare&utm_medium=link2&utm_source=sharebutton
- Team: 4 GitHub contributor(s) — Angelina Zhang (18 commits), Jillian Chang (13 commits), jenni-mori1 (6 commits), ionaxia2013 (5 commits)

## Devpost submission (written by the team)

### Inspiration

Considering the rapid growth of technology and the disconnection between many elderly people and their devices, we wanted to use technology for the better and help tackle a prevalent issue that many seniors encounter, for example the "Ore-Ore Sagi" scam in Japan. Our grandparents have fallen victim to scams before, so we felt personally inspired to help senior citizens who are often targeted by scammers for being more trusting and easier to scam.

### What it does

The scam detection works by using NLP to identify whether text resembles scam, and it is connected with an interactive quiz with both audio and text in a website.

### How we built it

We wanted to build everything onto a website, so we spent a lot of time using HTML and Javascript to create a beautiful website and an interesting quiz. As for the NLP model, we decided to use the BERT Machine Learning Framework due to its efficiency and accuracy when running on ambiguous language in text. With BERT, we worked on collecting a dataset (a combination of personally generated work and work from an existing catalog), preprocessing and tokenizing the data, and designing and testing the model.

### Challenges we ran into

We definitely faced a lot of challenges along the way. We decided to dive into a topic and techniques that none of us were extremely familiar with, but we thought that it would be cool. Because of this, we spent a lot of time understanding how frameworks such as BERT work, installing and uninstalling different packages in terminal, and scouring Stack Overflow for ways to debug. We also struggled to find ways to deploy our machine learning model in python to a web app to make them more accessible. While these challenges got infuriating at times, we were able to rise above and create something that we are pretty proud of.

### Accomplishments we're proud of

I think our biggest accomplishment was creating a cohesive website that connects the various parts that we have made. As stated above, one of the hardest parts was finding a way to connect our model to a website, and creating that was definitely one of our biggest accomplishments. Beyond that, for the majority of us, this was our first hackathon! Completing something that we all feel proud of is a big accomplishment in it of itself.

### What we learned

Learning to integrate different languages across different IDE's and computers with limited experience in a timed environment was stressful, but we enjoyed the learning and collaboration experience. We also learned the importance of time-management.

### What's next

Our next steps are including whisper.ai to implement speech-to-text capabilities to increase the impact of our project. We also want to include other languages to make our scam detection more inclusive.

## README (from the GitHub repository)

# noScamOnlyHam

Welcome to no scam, only ham! We created a scam detection package to help reduce the likelihood of getting scammed. We built a fun quiz as well as a NLP scam detection model.


## Detected evidence (automated analysis)

Indexed codebase: 10 recognized source files, 36 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- Flask (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (12 of 12)

```
.vscode/launch.json
app.py
hackathon.py
README.md
spamdataset.csv
static/quiz.html
static/script_quiz.js
static/script.js
static/style_quiz.css
static/style.css
templates/index.html
templates/index2.html
```

### Dependencies

No dependency index available.

### Recent commits (newest first)

- Update README.md
- Update README.md
- src
- Update hackathon.py
- edit
- bunch of edits
- Delete Dataset_59711.csv
- Delete quiz directory
- Delete scam_data.csv
- Delete home.html
- Delete info.html
- Update README.md
- Update hackathon.py
- clean up files
- add new html files
- added quiz
- added quiz files
- delete server.js
- Delete server.js
- Merge branch 'main' of https://github.com/jenni-mori1/psychic-goggles

## Key source files (fetched from GitHub, selected and truncated for size)

### app.py

```python
from flask import Flask, request, jsonify, render_template
import torch
from transformers import BertTokenizerFast, AutoModel
from hackathon import BERT_Arch  # Make sure this import points to where your BERT_Arch class is defined
import numpy as np

app = Flask(__name__)

# Setup device
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# Load the pre-trained BERT model
bert = AutoModel.from_pretrained('bert-base-uncased')

# Initialize your model with the BERT model
model = BERT_Arch(bert).to(device)
model.load_state_dict(torch.load("best_model.pth", map_location=device))
model.eval()

# Initialize your tokenizer
tokenizer = BertTokenizerFast.from_pretrained('bert-base-uncased')

@app.route('/', methods=['GET'])
def home():
    # Render the home template with the input form
    return render_template('index.html')

@app.route('/predict', methods=['POST'])
def predict():
    if request.method == 'POST':
        # Get text from the form
        text = request.form['text']

        # Tokenize and encode the text
        tokens = tokenizer.encode_plus(text, max_length=25, pad_to_max_length=True, truncation=True, return_tensors="pt")
        seq = tokens['input_ids'].to(device)
        mask = tokens['attention_mask'].to(device)
        
        with torch.no_grad():
            preds = model(seq, mask)
            preds = preds.detach().cpu().numpy()
            predicted_label = np.argmax(preds, axis=1)[0]
        
        # Convert numerical prediction back to string label
        label = 'scam' if predicted_label == 1 else 'ham'

        # Render the same or a different template with the prediction result
        return render_template('index.html', prediction_text=f'Predicted Label: {label}')

if __name__ == '__main__':
    app.run(debug=True)

```

### hackathon.py

```python
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
import transformers
from transformers import AutoModel, BertTokenizerFast
from transformers import AdamW
from sklearn.utils.class_weight import compute_class_weight
import copy

df = pd.read_csv("spamdataset.csv")
device = torch.device("mps")

df['Label'].value_counts(normalize = True)

bert = AutoModel.from_pretrained('bert-base-uncased')

# Load the BERT tokenizer
tokenizer = BertTokenizerFast.from_pretrained('bert-base-uncased')


train_text, temp_text, train_labels, temp_labels = train_test_split(df['Text'], df['Label'], 
                                                                    random_state=2018, 
                                                                    test_size=0.3, 
                                                                    stratify=df['Label'])


val_text, test_text, val_labels, test_labels = train_test_split(temp_text, temp_labels, 
                                                                random_state=2018, 
                                                                test_size=0.5, 
                                                                stratify=temp_labels)

# tokenize and encode sequences
tokens_train = tokenizer.batch_encode_plus(
    train_text.tolist(),
    max_length = 25,
    pad_to_max_length=True,
    truncation=True
)

tokens_val = tokenizer.batch_encode_plus(
    val_text.tolist(),
    max_length = 25,
    pad_to_max_length=True,
    truncation=True
)

tokens_test = tokenizer.batch_encode_plus(
    test_text.tolist(),
    max_length = 25,
    pad_to_max_length=True,
    truncation=True
)

train_seq = torch.tensor(tokens_train['input_ids'])
train_mask = torch.tensor(tokens_train['attention_mask'])
train_y = torch.tensor(train_labels.tolist())

val_seq = torch.tensor(tokens_val['input_ids'])
val_mask = torch.tensor(tokens_val['attention_mask'])
val_y = torch.tensor(val_labels.tolist())

test_seq = torch.tensor(tokens_test['input_ids'])
test_mask = torch.tensor(tokens_test['attention_mask'])
test_y = torch.tensor(test_labels.tolist())

print(test_seq, test_mask, len(test_seq[1]), len(test_seq[0]), len(test_mask[1]), len(test_mask[0]))

from torch.utils.data import TensorDataset, DataLoader, RandomSampler, SequentialSampler

batch_size = 32


train_data = TensorDataset(train_seq, train_mask, train_y)

train_sampler = RandomSampler(train_data)

train_dataloader = DataLoader(train_data, sampler=train_sampler, batch_size=batch_size)

val_data = TensorDataset(val_seq, val_mask, val_y)

val_sampler = SequentialSampler(val_data)

val_dataloader = DataLoader(val_data, sampler = val_sampler, batch_size=batch_size)

class BERT_Arch(nn.Module):

    def __init__(self, bert):
        super(BERT_Arch, self).__init__()
        
        self.bert = bert 
        
        self.dropout = nn.Dropout(0.1)
      
        self.relu =  nn.ReLU()

        self.fc1 = nn.Linear(768,512)
      
        self.fc2 = nn.Linear(512,2)

        self.softmax = nn.LogSoftmax(dim=1)

    

    def forward(self, sent_id, mask):
        _, cls_hs = self.bert(sent_id, attention_mask=mask, return_dict=False)
      
        x = self.fc1(cls_hs)

        x = self.relu(x)

        x = self.dropout(x)

        x = self.fc2(x)
      
        x = self.softmax(x)

        return x

model = BERT_Arch(bert)
model = model.to(device)

optimizer = AdamW(model.parameters(),lr = 1e-5)

#print(model.keys())
model.load_state_dict(torch.load("best_model.pth"))


'''

class_weights = compute_class_weight(class_weight = "balanced", classes = np.unique(train_labels), y = train_labels)

weights= torch.tensor(class_weights,dtype=torch.float)

weights = weights.to(device)

cross_entropy  = nn.NLLLoss(weight=weights)

model.eval()

dummy_input = torch.tensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 21,22, 23,24 ,25]])
dummy_mask = torch.tensor([[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 21,22, 23,24 ,25]])
inputs = (dummy_input.to(device), dummy_mask.to(device))


# Export the model to ONNX format
onnx_path = "best_model.onnx"

torch.onnx.export(model, inputs, onnx_path, verbose=True)

#used to train/test model

def evaluate():
    
    print("\nEvaluating...")
  
    # deactivate dropout layers
    model.eval()

    total_loss, total_accuracy = 0, 0
    
    # empty list to save the model predictions
    total_preds = []

    # iterate over batches
    for step,batch in enumerate(val_dataloader):
        
        # Progress update every 50 batches.
        if step % 50 == 0 and not step == 0:
            
            # Calculate elapsed time in minutes.
            elapsed = format_time(time.time() - t0)
            
            # Report progress.
            print('  Batch {:>5,}  of  {:>5,}.'.format(step, len(val_dataloader)))

        # push the batch to gpu
        batch = [t.to(device) for t in batch]

        sent_id, mask, labels = batch

        # deactivate autograd
        with torch.no_grad():
            
            # model predictions
            preds = model(sent_id, mask)

            # compute the validation loss between actual and predicted values
            loss = cross_entropy(preds,labels)

            total_loss = total_loss + loss.item()

            preds = preds.detach().cpu().numpy()

            total_preds.append(preds)

    # compute the validation loss of the epoch
    avg_loss = total_loss / len(val_dataloader) 

    # reshape the predictions in form of (number of samples, no. of classes)
    total_preds  = np.concatenate(total_preds, axis=0)

    return avg_loss, total_preds

best_valid_loss = float('inf')

#defining epochs
epochs = 5

# empty lists to store training and validation loss of each epoch
train_losses=[]
valid_losses=[]

def train():
    
    model.train()
    total_loss, total_accuracy = 0, 0
  
    # empty li
[truncated — 3308 more characters]
```

### static/script.js

```javascript
$(document).ready(function() {
	  $('.tabs li').click(function(){
	    if ($(this).hasClass('selected')===false) {
	      $('.tabs li').removeClass('selected');
	      $(this).addClass('selected');
	    }
	    var selectionId = $(this).attr('id');
	    $('.content').fadeOut('fast', function(){
	      $('div .page').css('display','none');
	      $('.page#'+selectionId).css('display','block');
	      $('.content').fadeIn('fast');
	    });
	  });
	});
```

### templates/index2.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Spam or Ham Prediction</title>
</head>
<body>
    <h2>Enter Text to Classify as Spam or Ham</h2>
    <form action="/predict" method="post">
        <textarea name="text" rows="4" cols="50" placeholder="Enter text here..."></textarea><br>
        <input type="submit" value="Submit">
    </form>
    {% if prediction_text %}
    <h3>{{ prediction_text }}</h3>
    {% endif %}
</body>
</html>

```

### static/quiz.html

```html
<!DOCTYPE html>
<html lang="en" >
<head>
  <meta charset="UTF-8">
  <title>Smishing Quiz</title>
  <link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.2/css/bootstrap.min.css'>
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'><link rel="stylesheet" href="./style_quiz.css">

</head>
<body>
<!-- partial:index.partial.html -->
<main class="container">
  <section id="smishing">
  </section>
</main>
<!-- partial -->
  <script src='https://cdnjs.cloudflare.com/ajax/libs/react/16.4.2/umd/react.production.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.4.2/umd/react-dom.production.min.js'></script><script  src="./script_quiz.js"></script>

</body>
</html>

```

### templates/index.html

```html
<!DOCTYPE html>
<html lang="en" >
<head>
  <meta charset="UTF-8">
  <title>CodePen - For Scam/Ham</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">

</head>
<body>
<!-- partial:index.partial.html -->
<html>

<head>
	<meta http-equivalent="Content-Type" content="text/html; charset=utf-8" />
	<link rel="stylesheet" type="text/css" href={{ url_for( 'static', filename='style.min.css' ) }}/>
	<title>No Scam Only Ham</title>
</head>

<body>
	<h1><a href= "/" style="text-decoration: none; color: inherit;">No Scam Only Ham</a></h1>
	<div class="tabbed-menu">
		<ul class="tabs">
			<li id="tab1" class="selected">Predict</li>
			<li id="tab2">Prevent</li>
			<li id="tab3">Educate</li>
		</ul>
		<div class="contentWrapper">
			<div class="content">
				<div class="page" id="tab1" style="display:block;"><form id = "form" action="/predict" method="post">
      <div class="form-container">
        <div class="textarea-group">
            <label for="string"/>
          <textarea rows="8" placeholder="Type or paste" name="text"></textarea>
        </div>

      </div>
      <div class="spacer"></div>
      <button type="submit">Scam or Ham?</button>
    </form> 
    {% if prediction_text %}
    <h3>{{ prediction_text }}</h3>
    {% endif %}
</div>
				<div class="page" id="tab2" style="display:none;">
					<p>Scam calls or texts are malignant forms of communication meant to deceive victims and obtain sensitive information and/or money.
</p>
					<p>Steps to prevent against scam:</p>
					<ol>
						<li>Be wary of suspicious text messages. Don't respond or interact.</li>
						<li>Do not click on hyperlinks or attachments in suspect messages.</li>
							<li>Take preventative measures to filter unwanted messages or block them before they reach you.</li>
<li>Never give your credit card, banking, Social Security, Medicare, or any other personal and sensitive information over the phone unless you initiated the call.
</li>
					</ol>
<p>Ham is harmless communication that is exchanged between different people. Ham is perfectly normal. We love social interaction.</p>
				</div>
				<div class="page" id="tab3" style="display:none;">
					<a href="{{ url_for('static', filename='quiz.html') }}">
    <button>Take Our Quiz!</button>
</a>
				</div>
			</div>
		</div>
	</div>
</body>

</html>
<!-- partial -->
   <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
	<script src="{{ url_for('static', filename='script.js')}}"></script>

</body>
</html>

```

### static/style.css

```css
@import url('https://fonts.googleapis.com/css2?family=Jost:wght@100;300;400;700&display=swap');

body {
	margin: 0;
	padding: 0;
	border: 0;
	--salmon-color: #db846e;
	--black-color: #333333;
	background-color: var(--salmon-color);
	outline: none;
	font-family: 'Jost', sans-serif;
}

.tt{
	font-family: monospace;
	color: black;
}


.spacer {
	height: 15px;
}

h1 {
	text-align: center;
	padding: 20px;
	margin: 0;
	color: white;
	font-size: 40px;
	text-transform: uppercase;
}

p {
	font-size: 16px;
	margin-top: 0px;
}

.page a {
	color: teal;
	text-decoration: none;
}

.page a:hover {
	text-decoration: underline;
}

.icon-text, .icon {
	vertical-align: middle;
	display: inline-block;
}

/* and form */
.tabbed-menu {
	font-size: 15px;
	width: 70%;
	margin: 0px auto;
	-webkit-box-shadow: 2px 17px 10px -11px rgba(0,0,0,0.75);
	-moz-box-shadow: 2px 17px 10px -11px rgba(0,0,0,0.75);
	box-shadow: 2px 17px 10px -11px rgba(0,0,0,0.75);
}

ul.tabs {
	text-align: center;
	list-style: none;
	position: relative;
	margin: 0;
	padding: 0;
	line-height: 26px;
}

ul.tabs li {
	padding: 10px 15px;
	background-color: #a6928d;
	border: 1px solid black;
	border-bottom: none;
	display: inline-block;
	width: 15%;
	-webkit-transition: all .3s ease-in-out;
	-moz-transition: all .3s ease-in-out;
	-o-transition: all .3s ease-in-out;
	-ms-transition: all .3s ease-in-out;
	transition: all .3s ease-in-out;
	border-top-left-radius: 10px;
	border-top-right-radius: 10px;
}

ul.tabs li a {
	text-decoration: none;
	color: #F8F8FF;
	font-size: 1.1em;
	text-shadow: 0 1px 0 rgba(255,255,255,.15);
}

ul.tabs li.selected {
	background-color: #f2f2f2;
	border-bottom-color: transparent;
	color: #696969;
	text-shadow: 0 1px 0 rgba(0,0,0,.75);
}

ul.tabs li:hover {
	cursor: pointer;
	background: #ccaba3;
}

ul.tabs li.selected:hover {
	background: #f2f2f2;
}

.page li {
	font-size: 16px;
	text-align: left;
}

.content {
	padding: 20px 25px;
}

.contentWrapper {
	border-top-left-radius: 10px;
	border-top-right-radius: 10px;
	background-color: #f2f2f2;
}

/* textarea + checkbox flexbox
*/
.form-container {
	display: flex;
	justify-content: center;
	gap: 20px;
}


.textarea-group {
	flex: 0 1 60%;
}

legend {
	font-size: 20px;
}

form textarea {
	width: 100%;
	padding: 10px 10px 10px 10px;
	box-sizing: border-box;
	border: 3px solid #ccc;
	color: #333;
	border-radius: 3px;
	resize: none;
	font-size: 16px;
	font-family: inherit;
}


button {
  border: 1px;
  border-radius: 9px;
  color: #ffffff;
  font-size: 1rem;
  font-weight: 700;
  text-transform: uppercase;
  line-height: 1;
  padding: 1.2em 2.8em;
  background: #c9ab63;
  text-decoration: none;
  text-align: center;
  display: block;
  margin: 0 auto;
}

button:hover {
  background: #947d47;
  text-decoration: none;
}


@media screen and (max-width: 1050px) {
    form textarea {
      min-height:280px;
    }
  }
  @media screen and (max-width: 800px) {
    ul.tabs li {
             font-size: 14px;
             width: 20%;
    }
    .tabbed-menu {
             width: 96%;
     }
  }
```

### static/style_quiz.css

```css
@font-face {
  font-family: 'Open Sans';
  font-style: normal;
  font-weight: 400;
  font-stretch: normal;
  src: url(https://fonts.gstatic.com/s/opensans/v40/memSYaGs126MiZpBA-UvWbX2vVnXBbObj2OVZyOOSr4dVJWUgsjZ0B4gaVc.ttf) format('truetype');
}
@keyframes roll-in {
  0% {
    top: 10px;
    opacity: 0;
  }
  100% {
    top: 0;
    opacity: 1;
  }
}
@keyframes fade {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
@keyframes pulse {
  from {
    transform: scale3d(1, 1, 1);
  }
  50% {
    transform: scale3d(1.05, 1.05, 1.05);
  }
  to {
    transform: scale3d(1, 1, 1);
  }
}
.pulse {
  animation: pulse 1s infinite;
}
.fade-in {
  animation: fade 0.75s ease;
}
.quiz {
  margin: 2em auto;
  min-height: 40vh;
  font-size: 16px;
}
.quiz .progress {
  position: relative;
  transition: width 0.4s ease;
  margin-bottom: 1em;
  background: #b5b5b5;
  border-radius: 0;
  width: 100%;
  height: 2em;
  font-family: "Open Sans", "Helvetica", "Arial", sans-serif;
}
.quiz .progress .progress-bar {
  background-color: #db846e;
}
.quiz .progress .counter {
  position: absolute;
  right: 5px;
  top: 0;
  font-weight: normal;
  color: #fff;
  height: 100%;
  font-family: "Open Sans", "Helvetica", "Arial", sans-serif;
  font-size: 1.25em;
  margin: auto 0.5em;
  letter-spacing: 0.025em;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.quiz form {
  width: 90%;
  margin: 1.5em auto;
}
.quiz .img-fluid {
  margin: 2em auto;
  max-width: 360px;
  display: block;
}
.quiz .question {
  font-weight: bold;
  line-height: 1.35;
  margin-bottom: 0.75em;
}
.quiz .option {
  margin-bottom: 0.25em;
  transition: all 0.25s ease;
  font-size: 0.9em;
}
.quiz button {
  padding: 0.75em;
  font-family: "Open Sans", "Helvetica", "Arial", sans-serif;
  background-color: #db846e;
  border: 0;
  color: #fff;
  font-size: 1em;
  transition: 0.25s all;
  white-space: nowrap;
  font-weight: bold;
  cursor: pointer;
}
.quiz button i {
  margin-left: 0.15em;
}
.quiz button:disabled {
  opacity: 0.5;
}
.quiz input[type="radio"] {
  position: absolute;
  left: -9999px;
}
.quiz input[type="radio"] + label {
  position: relative;
  font-weight: normal;
  padding-left: 28px;
  cursor: pointer;
  line-height: 20px;
  display: inline-block;
  color: #666;
}
.quiz input[type="radio"] + label::before {
  text-align: center;
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  width: 20px;
  height: 20px;
  border: 1px solid #ddd;
  border-radius: 100%;
  background: #fff;
}
.quiz input[type="radio"] + label::after {
  content: '';
  width: 12px;
  height: 12px;
  background-color: #222;
  position: absolute;
  top: 4px;
  left: 4px;
  border-radius: 100%;
  transition: all 0.2s ease;
}
.quiz .dim input[type="radio"] + label::before,
.quiz .correct input[type="radio"] + label::before {
  border: 0;
  font-size: 1.2em;
  animation: 0.25s roll-in ease;
}
.quiz .dim input[type="radio"] + label::after,
.quiz .correct input[type="radio"] + label::after {
  display: none;
}
.quiz .correct input[type="radio"] + label:before {
  content: '\f00C';
  font-family: "FontAwesome" !important;
  color: #36ad3b;
}
.quiz .dim input[type="radio"]:checked + label:before {
  content: '\f00d';
  font-family: "FontAwesome" !important;
  color: #ff1100;
}
.quiz input[type="radio"]:not(:checked) + label:after {
  opacity: 0;
  transform: scale(0);
}
.quiz input[type="radio"]:checked + label:after {
  opacity: 1;
  transform: scale(1);
}
.quiz .dim {
  opacity: 0.5;
}
.quiz .bottom {
  width: 90%;
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
}
.quiz .bottom div {
  flex: 1 1 70%;
  font-size: 0.9em;
}
.quiz .bottom .next {
  flex: 0 1 10%;
  margin-left: 3em;
}
@media (max-width: 600px) {
  .quiz .bottom div,
  .quiz .bottom .next {
    flex-basis: 100%;
  }
  .quiz .bottom .next {
    margin-left: 0;
  }
}
.quiz .get-results {
  display: block;
  margin: 2em auto;
}
.quiz .results {
  font-size: 1.1em;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  min-height: 40vh;
}
.quiz .results h1 {
  font-family: "Open Sans", "Helvetica", "Arial", sans-serif;
}
.quiz .results button {
  margin-top: 1em;
}
```

### static/script_quiz.js

```javascript
//render raw HTML from question data 
const RawHTML = props => /*#__PURE__*/React.createElement("span", { dangerouslySetInnerHTML: { __html: props.html } });

class QuestionMedia extends React.Component {
  render() {
    const { img, audio } = this.props;

    if (img) {
      return /*#__PURE__*/React.createElement("img", { className: "img-fluid", src: img.src, alt: img.alt });
    } else if (audio) {
      return /*#__PURE__*/(
        React.createElement("audio", { controls: true }, /*#__PURE__*/
        React.createElement("source", { src: audio.src, type: "audio/mpeg" }), "Your browser does not support the audio element."));



    } else {
      return null; // No media to display
    }
  }}



const QuizProgress = props => {
  return /*#__PURE__*/(
    React.createElement("div", { className: "progress" }, /*#__PURE__*/
    React.createElement("p", { className: "counter" }, /*#__PURE__*/
    React.createElement("span", null, "Question ", props.currentQuestion + 1, " of ", props.questionLength)), /*#__PURE__*/

    React.createElement("div", { className: "progress-bar", style: { 'width': (props.currentQuestion + 1) / props.questionLength * 100 + '%' } })));


};

const Results = props => {
  return /*#__PURE__*/(
    React.createElement("div", { className: "results fade-in" }, /*#__PURE__*/
    React.createElement("h1", null, "Your score: ", (props.correct / props.questionLength * 100).toFixed(), "%"), /*#__PURE__*/
    React.createElement("button", { type: "button", onClick: props.startOver }, "Try again ", /*#__PURE__*/React.createElement("i", { className: "fas fa-redo" }))));


};

class Quiz extends React.Component {
  constructor(props) {
    super(props);

    this.updateAnswer = this.updateAnswer.bind(this);
    this.checkAnswer = this.checkAnswer.bind(this);
    this.nextQuestion = this.nextQuestion.bind(this);
    this.getResults = this.getResults.bind(this);
    this.startOver = this.startOver.bind(this);

    this.state = {
      currentQuestion: 0,
      correct: 0,
      inProgress: true,
      questions: [{
        question: "Is this scam or ham?",
        options: [{
          option: "Scam",
          correct: true },
        {
          option: "Ham",
          correct: false }],

        img: {
          src: 'https://iili.io/JEkFDg9.png',
          alt: 'biden' },

        feedback: "There are several red flags indicating that this message is likely a scam. The use of urgency and exclamation marks ('Stand up America!') is a common tactic that scammers use to pressure individuals into taking action without thinking. Additionally, this message is promising a large sum of money from the government without an official announcement, which is to be taken with caution. Finally, the link looks highly suspicious, as an announcement from the government should come from a .gov website.",
        moreUrl: 'https://consumer.ftc.gov/articles/robocalls' },
      {
        question: "Is this spam or ham?",
        options: [{
          option: "Spam",
          correct: false },
        {
          option: "Ham",
          correct: true }],

        img: {
          src: 'https://iili.io/JEkqWJ9.png',
          alt: 'David and Grandma' },

        feedback: "This text message is likely not scam because it's personalized ('Hi, Grandma, it's David').There are no demands for action, suspicious links, or requests for personal information. It's always wise to verify the sender's identity if you're unsure, but this message appears to be a heartfelt check-in rather than a malicious attempt." },
      {
        question: "Is this spam or ham?",
        options: [{
          option: "Spam",
          correct: true },
        {
          option: "Ham",
          correct: false }],

        img: {
          src: 'https://iili.io/JEkBNkX.png',
          alt: 'wells fargo spam' },

        feedback: "This text message is likely scam. It prompts the recipient to call a phone number that doesn't resemble Wells Fargo's official contact information. Also, the request to ignore the message if it's considered valid is also unusual for a legitimate communication from a bank.",
        moreUrl: 'https://www.wellsfargo.com/privacy-security/fraud/report/phish/#:~:text=Be%20suspicious%20of%20messages%20that,urge%20to%20respond%20right%20away' },
      {
        question: "Is this spam or ham?",
        options: [{
          option: "Spam",
          correct: false },
        {
          option: "Ham",
          correct: true }],

        img: {
          src: 'https://iili.io/JEkCxaI.png',
          alt: 'wellsfargo not fraud' },

        feedback: "This one is more ambiguous, but it appears to be legitimate. Compared to the previous Wells Fargo message, this one provides the correct phone number. Additionally, the option to stop receiving messages by replying 'STOP' is a standard feature offered by legitimate messaging services. However, one must still exercise with caution.",
        moreUrl: 'https://www.wellsfargo.com/privacy-security/fraud/report/phish/' },
      {
        question: "Is this spam or ham?",
        options: [{
          option: "Spam",
          correct: true },
        {
          option: "Ham",
          correct: false }],

        img: {
          src: 'https://iili.io/JEko9mF.png',
          alt: 'Stanford' },

        feedback: "This text message is scam. While it claims to be  'Stanford FCU,' it lacks the identification details one would expect from a legitimate message, such as the recipient's name or specific account information. The provided link also looks like a suspicious website that is not associated with Stanford FCU. ",
        moreUrl: 'https://uit.stanford.edu/news/phishing-scams-often-target-stanford-students' },
      {
        question: "Listen to this phone call. Is it scam or ham?",
        options: [{
          option: "Scam",
          correct: true },
        {
          option: "Ham",
          correct: false }],

        audio: {
          src: 'https://
[truncated — 8119 more characters]
```