Tabrejmlkhan commited on
Commit
e24c25f
β€’
1 Parent(s): e271a97

Upload 7 files

Browse files
Procfile ADDED
@@ -0,0 +1 @@
 
 
1
+ web: gunicorn app:app
app.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ο»Ώfrom flask import Flask, request, render_template, redirect, url_for
2
+ from transformers import AutoTokenizer, AutoModel
3
+ import torch
4
+ import os
5
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
6
+ app = Flask(__name__)
7
+
8
+ # Dictionary to store programs and their courses
9
+ programs = {}
10
+
11
+ # Default model name
12
+ current_model_name = 'sentence-transformers/all-mpnet-base-v2'
13
+
14
+ # Function to load the tokenizer and model dynamically
15
+ def load_model_and_tokenizer(model_name):
16
+ try:
17
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
18
+ model = AutoModel.from_pretrained(model_name)
19
+ return tokenizer, model, None
20
+ except Exception as e:
21
+ return None, None, str(e)
22
+
23
+ # Load the initial model and tokenizer
24
+ tokenizer, model, error = load_model_and_tokenizer(current_model_name)
25
+
26
+ def mean_pooling(token_embeddings, mask):
27
+ """Applies mean pooling to token embeddings, considering the mask."""
28
+ mask = mask.unsqueeze(-1).expand(token_embeddings.size())
29
+ sum_embeddings = torch.sum(token_embeddings * mask, dim=1)
30
+ sum_mask = torch.clamp(mask.sum(dim=1), min=1e-9) # Avoid division by zero
31
+ return sum_embeddings / sum_mask
32
+
33
+ def compute_plo_embeddings():
34
+ """Computes embeddings for the predefined PLOs."""
35
+ tokens = tokenizer(plos, padding=True, truncation=True, return_tensors='pt')
36
+ mask = tokens['attention_mask']
37
+ with torch.no_grad():
38
+ outputs = model(**tokens)
39
+ return mean_pooling(outputs.last_hidden_state, mask)
40
+
41
+ # Predefined Program Learning Outcomes (PLOs)
42
+ plos = [
43
+ "Analyze a complex computing problem and apply principles of computing and other relevant disciplines to identify solutions.",
44
+ "Design, implement, and evaluate a computing-based solution to meet a given set of computing requirements.",
45
+ "Communicate effectively in a variety of professional contexts.",
46
+ "Recognize professional responsibilities and make informed judgments in computing practice based on legal and ethical principles.",
47
+ "Function effectively as a member or leader of a team engaged in activities appropriate to the program’s discipline.",
48
+ "Support the delivery, use, and management of information systems within an information systems environment."
49
+ ]
50
+
51
+ # Compute PLO embeddings (once at startup)
52
+ plo_embeddings = compute_plo_embeddings()
53
+
54
+ def get_similarity(input_sentence):
55
+ """Calculates the similarity between an input sentence and predefined PLOs."""
56
+ tokens = tokenizer(input_sentence, padding=True, truncation=True, return_tensors='pt')
57
+ mask = tokens['attention_mask']
58
+
59
+ with torch.no_grad():
60
+ outputs = model(**tokens)
61
+ input_embedding = mean_pooling(outputs.last_hidden_state, mask)
62
+
63
+ similarities = torch.nn.functional.cosine_similarity(input_embedding, plo_embeddings)
64
+ return similarities
65
+
66
+ @app.route('/')
67
+ def index():
68
+ """Home page displaying current programs and model status."""
69
+ return render_template('index.html', programs=programs, model_name=current_model_name)
70
+
71
+ @app.route('/set_model', methods=['POST'])
72
+ def set_model():
73
+ """Allows users to dynamically change the model."""
74
+ global tokenizer, model, plo_embeddings, current_model_name
75
+
76
+ model_name = request.form['model_name']
77
+ tokenizer, model, error = load_model_and_tokenizer(model_name)
78
+
79
+ if error:
80
+ return render_template('index.html', programs=programs, message=f"Error loading model: {error}")
81
+
82
+ # Update the global model name and recompute embeddings
83
+ current_model_name = model_name
84
+ plo_embeddings = compute_plo_embeddings()
85
+ return redirect(url_for('index'))
86
+
87
+ @app.route('/addprogram', methods=['GET', 'POST'])
88
+ def add_program():
89
+ """Adds a new program."""
90
+ if request.method == 'POST':
91
+ program_name = request.form['program_name']
92
+ if program_name not in programs:
93
+ programs[program_name] = {} # Initialize an empty dictionary for courses
94
+ return redirect(url_for('index'))
95
+ return render_template('addprogram.html')
96
+
97
+ @app.route('/addcourse', methods=['GET', 'POST'])
98
+ def create_course():
99
+ """Creates a new course under a specific program."""
100
+ if request.method == 'POST':
101
+ program_name = request.form['program']
102
+ course_name = request.form['course_name']
103
+ outcomes = request.form['course_outcomes'].split('\n')
104
+
105
+ if program_name in programs:
106
+ programs[program_name][course_name] = outcomes # Add course to the selected program
107
+
108
+ return redirect(url_for('index'))
109
+ return render_template('addcourse.html', programs=programs)
110
+
111
+ @app.route('/match', methods=['POST'])
112
+ def match_outcomes():
113
+ """Matches course outcomes with predefined PLOs."""
114
+ course_name = request.form['course']
115
+ print(course_name)
116
+ course_outcomes = request.form['course_outcomes'].split('\n')
117
+ results = []
118
+
119
+ for co in course_outcomes:
120
+ co = co.strip()
121
+ if co: # Ensure the outcome is not empty
122
+ similarities = get_similarity(co)
123
+ top_matches_indices = similarities.topk(3).indices.tolist()
124
+ results.append({
125
+ 'course_outcome': co,
126
+ 'course_name' : course_name,
127
+ 'best_matches': top_matches_indices
128
+ })
129
+
130
+ return render_template('result.html', course_name =course_name, results=results)
131
+
132
+ if __name__ == '__main__':
133
+ app.run(debug=True)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ Flask
2
+ transformers
3
+ torch
4
+ Gunicorn
5
+ pandas
6
+ openpyxl
templates/addcourse.html ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Add Course</title>
6
+ </head>
7
+ <body>
8
+ <h1>Add a New Course</h1>
9
+ <form action="/addcourse" method="post">
10
+ <label for="program">Select Program:</label><br>
11
+ <select id="program" name="program" required>
12
+ <option value="">--Select a program--</option>
13
+ {% for program in programs %}
14
+ <option value="{{ program }}">{{ program }}</option>
15
+ {% endfor %}
16
+ </select><br><br>
17
+
18
+ <label for="course_name">Course Name:</label><br>
19
+ <input type="text" id="course_name" name="course_name" required><br><br>
20
+
21
+ <label for="course_outcomes">Course Learning Outcomes (one per line):</label><br>
22
+ <textarea id="course_outcomes" name="course_outcomes" rows="5" cols="50" required></textarea><br><br>
23
+
24
+ <input type="submit" value="Add Course">
25
+ </form>
26
+ <br>
27
+ <a href="/">Back to Home</a>
28
+ </body>
29
+ </html>
templates/addprogram.html ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Add Program</title>
7
+ </head>
8
+ <body>
9
+ <h1>Add a New Program</h1>
10
+ <form action="/addprogram" method="post">
11
+ <label for="program_name">Program Name:</label><br>
12
+ <input type="text" id="program_name" name="program_name" required><br><br>
13
+ <input type="submit" value="Add Program">
14
+ </form>
15
+ <br>
16
+ <a href="/">Back to Home</a>
17
+ </body>
18
+ </html>
templates/index.html ADDED
@@ -0,0 +1,289 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
7
+ <title>Fibrosis</title>
8
+ <link rel="shortcut icon"
9
+ href="" />
10
+ <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
11
+ <style>
12
+ body{background-color: #eff2f9;}
13
+ .iupload h3{color: #1b2d6b;font-size: 30px;font-weight: 700;}
14
+ .img-part{height:300px;width:300px;margin:0px auto;}
15
+ .image-part{height:300px;width:300px;border:1px solid #1b2d6b;}
16
+ .image-part img{position:absolute;height: 300px;width:300px;display:none;padding:5px;}
17
+ .image-part #video{display:block;height: 300px;width:300px;padding:5px;}
18
+ .res-part{border:1px solid #dedede;margin-left:20px;height: 310px;width:100%;padding:5px;margin:0px auto;overflow:auto;}
19
+ .res-part2{border:1px solid #dedede;height: 310px;width:100%;padding:5px;margin:0px auto;}
20
+ .resp-img{height: 298px;width: 233px;margin:0px auto;}
21
+ .jsonRes{margin-left:30px;}
22
+ #send{cursor:pointer;}
23
+ .btn-part{width:325px;}
24
+ textarea,
25
+ select,
26
+ .form-control,
27
+ .custom-select,
28
+ button.btn,
29
+ .btn-primary,
30
+ input[type="text"],
31
+ input[type="url"],
32
+ .uneditable-input{
33
+ border: 1px solid #363e75;
34
+ outline: 0 !important;
35
+ border-radius:0px;
36
+ box-shadow: none;
37
+ -webkit-box-shadow: none;
38
+ -moz-box-shadow: none;
39
+ -moz-transition: none;
40
+ -webkit-transition: none;
41
+ }
42
+ textarea:focus,
43
+ select:focus,
44
+ .form-control:focus,
45
+ .btn:focus,
46
+ .btn-primary:focus,
47
+ .custom-select:focus,
48
+ input[type="text"]:focus,
49
+ .uneditable-input:focus{
50
+ border: 1px solid #007bff;
51
+ outline: 0 !important;
52
+ border-radius:0px;
53
+ box-shadow: none;
54
+ -webkit-box-shadow: none;
55
+ -moz-box-shadow: none;
56
+ -moz-transition: none;
57
+ -webkit-transition: none;
58
+ }
59
+ #loading {
60
+ position: fixed;
61
+ left: 0px;
62
+ top: 0px;
63
+ width: 100%;
64
+ height: 100%;
65
+ z-index: 9999999999;
66
+ overflow: hidden;
67
+ background: rgba(255, 255, 255, 0.7);
68
+ }
69
+ .loader {
70
+ border: 8px solid #f3f3f3;
71
+ border-top: 8px solid #363e75;
72
+ border-radius: 50%;
73
+ width: 60px;
74
+ height: 60px;
75
+ left: 50%;
76
+ margin-left: -4em;
77
+ display: block;
78
+ animation: spin 2s linear infinite;
79
+ }
80
+ .loader,
81
+ .loader:after {display: block;position: absolute;top: 50%;margin-top: -4.05em;}
82
+ @keyframes spin {
83
+ 0% {
84
+ transform: rotate(0deg);
85
+ }
86
+ 100% {
87
+ transform: rotate(360deg);
88
+ }
89
+ }
90
+ .right-part{border:1px solid #dedede;padding:5px;}
91
+ .logo{position:absolute;right:0px;bottom:0px;margin-right:30px;margin-bottom:30px;}
92
+ </style>
93
+ <script>
94
+ let programs = {{ programs|tojson }}; // Passing programs from the backend as a JSON object
95
+
96
+ // Update the courses dropdown when a program is selected
97
+ function updateCourses() {
98
+ const programSelect = document.getElementById("program");
99
+ const courseSelect = document.getElementById("course");
100
+ const selectedProgram = programSelect.value;
101
+
102
+ // Clear existing options in the course dropdown
103
+ courseSelect.innerHTML = "<option value=''>--Select a course--</option>";
104
+ document.getElementById("course_outcomes").value = ""; // Clear the course outcomes
105
+
106
+ if (selectedProgram && programs[selectedProgram]) {
107
+ const courses = Object.keys(programs[selectedProgram]);
108
+ courses.forEach(course => {
109
+ let option = document.createElement("option");
110
+ option.value = course;
111
+ option.text = course;
112
+ courseSelect.appendChild(option);
113
+ });
114
+ }
115
+ }
116
+
117
+ // Update the course outcomes in the listbox when a course is selected
118
+ function updateCourseOutcomes() {
119
+ const programSelect = document.getElementById("program").value;
120
+ const courseSelect = document.getElementById("course").value;
121
+ const courseOutcomes = programs[programSelect] ? programs[programSelect][courseSelect] : [];
122
+
123
+ const courseOutcomesTextArea = document.getElementById("course_outcomes");
124
+ courseOutcomesTextArea.value = courseOutcomes ? courseOutcomes.join("\n") : "";
125
+ }
126
+ </script>
127
+ </head>
128
+ <body>
129
+ <div class="main container">
130
+ <section class="iupload">
131
+ <h3 class="text-center py-4">CLOs - PLOs Mapping Using Large Language Models </h3>
132
+ <div class="row">
133
+ <div class="img-part col-md-6">
134
+ <h1> </h1>
135
+
136
+ <!-- Section for Model Selection -->
137
+ <h5>Select Model</h5>
138
+ <form action="/set_model" method="post">
139
+ <label for="model_name">Choose a Model:</label><br>
140
+ <select id="model_name" name="model_name">
141
+ <option value="sentence-transformers/all-mpnet-base-v2" selected>all-mpnet-base-v2</option>
142
+ <option value="sentence-transformers/bert-base-nli-mean-tokens">bert-base-nli-mean-tokens</option>
143
+ <option value="thuan9889/llama_embedding_model_v1">llama_embedding_model_v1</option>
144
+ <option value="sembeddings/model_gpt_trained">model_gpt_trained</option>
145
+ </select>
146
+ <button type="submit">Set Model</button>
147
+ </form>
148
+
149
+ <h5>Current Model: {{ model_name }}</h5>
150
+ {% if message %}
151
+ <p style="color: red;">{{ message }}</p>
152
+ {% endif %}
153
+
154
+ <hr>
155
+
156
+ <!-- Section for Course Outcome Matching -->
157
+ <h5>Select Program and Course</h5>
158
+ <form action="/match" method="post">
159
+ <label for="program">Select Program:</label><br>
160
+ <select id="program" name="program" onChange="updateCourses()">
161
+ <option value="">--Select a program--</option>
162
+ {% for program in programs %}
163
+ <option value="{{ program }}">{{ program }}</option>
164
+ {% endfor %}
165
+ </select><br><br>
166
+
167
+ <label for="course">Select Course:</label><br>
168
+ <select id="course" name="course" onChange="updateCourseOutcomes()">
169
+ <option value="">--Select a course--</option>
170
+ </select><br><br>
171
+
172
+ <label for="course_outcomes">Course Outcomes:</label><br>
173
+ <textarea id="course_outcomes" name="course_outcomes" rows="10" cols="50" readonly></textarea><br><br>
174
+
175
+ <input type="submit" value="Submit">
176
+ </form>
177
+ </div>
178
+ <div class="col-md-6 col-xs-12 right-part">
179
+ <h5 class="mb-2"><center>Create a Program or Course</center></h5>
180
+ <div class="row">
181
+
182
+ <div class="res-part col-md-5 col-xs-12"><div class="jsonRes"><h2></h2>
183
+ <ul>
184
+ <li><a href="/addprogram">Create Program</a></li>
185
+ <li><a href="/addcourse">Create Course</a></li>
186
+ </ul></div></div>
187
+ </div>
188
+ </div>
189
+ </div>
190
+ </section>
191
+ </div>
192
+
193
+ <img class="logo" src="" />
194
+
195
+
196
+ <div id="loading"></div>
197
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
198
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
199
+ <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
200
+
201
+ <script>
202
+ var mybtn = document.getElementById('startbtn');
203
+ var myvideo = document.getElementById('video');
204
+ var mycanvas = document.getElementById('canvas');
205
+ var myphoto = document.getElementById('photo');
206
+ var base_data = "";
207
+
208
+ function sendRequest(base64Data){
209
+ var type = "json";
210
+ if(base64Data != "" || base64Data != null){
211
+ if(type == "imgtobase"){
212
+ $(".res-part").html("");
213
+ $(".res-part").html(base64Data);
214
+ }
215
+ else if(type == "basetoimg"){
216
+ var imageData = $("#imgstring").val();
217
+ $(".res-part").html("");
218
+ $(".res-part").append("<img src='data:image/jpeg;base64," + imageData + "' alt='' />");
219
+ }
220
+ else{
221
+ var url = $("#url").val();
222
+ $("#loading").show();
223
+ $.ajax({
224
+ url : url,
225
+ type: "post",
226
+ cache: false,
227
+ async: true,
228
+ crossDomain: true,
229
+ headers: {
230
+ 'Content-Type': 'application/json',
231
+ 'Access-Control-Allow-Origin':'*'
232
+ },
233
+ data:JSON.stringify({image:base64Data}),
234
+ success: function(res){
235
+ $(".res-part").html("");
236
+ $(".res-part2").html("");
237
+ try{
238
+ var imageData = res[1].image;
239
+ if(imageData.length > 100){
240
+ if(imageData.length > 10){$(".res-part2").append("<img class='resp-img' src='data:image/jpeg;base64," + imageData + "' alt='' />");}
241
+ }
242
+ }catch(e){}
243
+ $(".res-part").html("<pre>" + JSON.stringify(res[0], undefined, 2) + "</pre>");
244
+ $("#loading").hide();
245
+ }
246
+ });
247
+ }
248
+ }
249
+ }
250
+
251
+ $(document).ready(function(){
252
+ $("#loading").hide();
253
+
254
+ $('#send').click(function(evt){
255
+ sendRequest(base_data);
256
+ });
257
+
258
+ $('#uload').click(function(evt) {
259
+ $('#fileinput').focus().trigger('click');
260
+ });
261
+ $("#fileinput").change(function(){
262
+ if (this.files && this.files[0]){
263
+ var reader = new FileReader();
264
+ reader.onload = function (e){
265
+ var url = e.target.result;
266
+ var img = new Image();
267
+ img.crossOrigin = 'Anonymous';
268
+ img.onload = function(){
269
+ var canvas = document.createElement('CANVAS');
270
+ var ctx = canvas.getContext('2d');
271
+ canvas.height = this.height;
272
+ canvas.width = this.width;
273
+ ctx.drawImage(this, 0, 0);
274
+ base_data = canvas.toDataURL('image/jpeg', 1.0).replace(/^data:image.+;base64,/, '');
275
+ canvas = null;
276
+ };
277
+ img.src = url;
278
+ $('#photo').attr('src', url);
279
+ $('#photo').show();
280
+ $('#video').hide();
281
+ }
282
+ reader.readAsDataURL(this.files[0]);
283
+ }
284
+ });
285
+ });
286
+
287
+ </script>
288
+ </body>
289
+ </html>
templates/result.html ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Predicted Mapping</title>
7
+ <style>
8
+ table, th, td {
9
+ border: 1px solid black;
10
+ border-collapse: collapse;
11
+ padding: 10px;
12
+ text-align: left;
13
+ }
14
+ .top-1 { background-color: red; }
15
+ .top-2 { background-color: blue; }
16
+ .top-3 { background-color: green; }
17
+ </style>
18
+ </head>
19
+ <body>
20
+ <h1>Predicted Mapping</h1>
21
+ <h2>Course: {{ course_name }}</h2>
22
+ <table>
23
+ <tr>
24
+ <th>Course Learning Outcome</th>
25
+ <th>PLO1</th>
26
+ <th>PLO2</th>
27
+ <th>PLO3</th>
28
+ <th>PLO4</th>
29
+ <th>PLO5</th>
30
+ <th>PLO6</th>
31
+ </tr>
32
+ {% for result in results %}
33
+ <tr>
34
+ <td>{{ result.course_outcome }}</td>
35
+ <td class="{% if 0 in result.best_matches[:1] %}top-1{% elif 0 in result.best_matches[1:2] %}top-2{% elif 0 in result.best_matches[2:3] %}top-3{% endif %}">{{ '√' if 0 in result.best_matches else '' }}</td>
36
+ <td class="{% if 1 in result.best_matches[:1] %}top-1{% elif 1 in result.best_matches[1:2] %}top-2{% elif 1 in result.best_matches[2:3] %}top-3{% endif %}">{{ '√' if 1 in result.best_matches else '' }}</td>
37
+ <td class="{% if 2 in result.best_matches[:1] %}top-1{% elif 2 in result.best_matches[1:2] %}top-2{% elif 2 in result.best_matches[2:3] %}top-3{% endif %}">{{ '√' if 2 in result.best_matches else '' }}</td>
38
+ <td class="{% if 3 in result.best_matches[:1] %}top-1{% elif 3 in result.best_matches[1:2] %}top-2{% elif 3 in result.best_matches[2:3] %}top-3{% endif %}">{{ '√' if 3 in result.best_matches else '' }}</td>
39
+ <td class="{% if 4 in result.best_matches[:1] %}top-1{% elif 4 in result.best_matches[1:2] %}top-2{% elif 4 in result.best_matches[2:3] %}top-3{% endif %}">{{ '√' if 4 in result.best_matches else '' }}</td>
40
+ <td class="{% if 5 in result.best_matches[:1] %}top-1{% elif 5 in result.best_matches[1:2] %}top-2{% elif 5 in result.best_matches[2:3] %}top-3{% endif %}">{{ '√' if 5 in result.best_matches else '' }}</td>
41
+ </tr>
42
+ {% endfor %}
43
+ </table>
44
+ <a href="/">Back to Input</a>
45
+ </body>
46
+ </html>