pujan paudel commited on
Commit ·
1a11305
1
Parent(s): d88cc48
first commit
Browse files- __init__.py +0 -0
- __pycache__/__init__.cpython-311.pyc +0 -0
- __pycache__/app.cpython-311.pyc +0 -0
- __pycache__/model.cpython-311.pyc +0 -0
- app.py +96 -0
- canvas.jpeg +0 -0
- dockerfile +11 -0
- model.py +169 -0
- requirements.txt +8 -0
- static/banner.png +0 -0
- static/clear.png +0 -0
- static/logo.png +0 -0
- static/script/dialog.js +87 -0
- static/script/faq.js +10 -0
- static/script/metrices.js +0 -0
- static/script/nav.js +14 -0
- static/script/plotly.js +0 -0
- static/script/popup.js +33 -0
- static/script/script.js +260 -0
- static/style/about.css +93 -0
- static/style/dialog.css +121 -0
- static/style/help.css +53 -0
- static/style/home.css +90 -0
- static/style/info-panel.css +57 -0
- static/style/navbar.css +71 -0
- static/style/popup.css +50 -0
- static/style/style.css +64 -0
- templates/about.html +102 -0
- templates/components/navbar.html +13 -0
- templates/help.html +63 -0
- templates/index.html +124 -0
- templates/layout.html +13 -0
__init__.py
ADDED
|
File without changes
|
__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (157 Bytes). View file
|
|
|
__pycache__/app.cpython-311.pyc
ADDED
|
Binary file (4.37 kB). View file
|
|
|
__pycache__/model.cpython-311.pyc
ADDED
|
Binary file (11 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io
|
| 2 |
+
import time
|
| 3 |
+
import cv2
|
| 4 |
+
from flask import Flask, render_template, request, redirect, url_for, flash,jsonify
|
| 5 |
+
from PIL import Image # For image processing (optional)
|
| 6 |
+
import os
|
| 7 |
+
import base64
|
| 8 |
+
import json
|
| 9 |
+
from .model import predict
|
| 10 |
+
|
| 11 |
+
ALLOWED_EXTENSIONS = {'jpg', 'jpeg'}
|
| 12 |
+
|
| 13 |
+
app = Flask(__name__)
|
| 14 |
+
app.secret_key='your_secret_key'
|
| 15 |
+
|
| 16 |
+
def allowed_file(filename):
|
| 17 |
+
return '.' in filename and \
|
| 18 |
+
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
| 19 |
+
|
| 20 |
+
@app.route('/')
|
| 21 |
+
def home():
|
| 22 |
+
return render_template('index.html')
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@app.route('/help')
|
| 26 |
+
def help():
|
| 27 |
+
return render_template('help.html')
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@app.route('/about')
|
| 31 |
+
def aboutus():
|
| 32 |
+
return render_template('about.html')
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@app.route('/upload', methods=['POST'])
|
| 36 |
+
def upload_image():
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
if 'file-input' in request.files:
|
| 40 |
+
uploaded_file = request.files['file-input']
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
if uploaded_file and allowed_file(uploaded_file.filename):
|
| 44 |
+
|
| 45 |
+
filename= uploaded_file.filename
|
| 46 |
+
|
| 47 |
+
try:
|
| 48 |
+
# Read and validate the image (modify as needed)
|
| 49 |
+
image_buffer=io.BytesIO(uploaded_file.read())
|
| 50 |
+
|
| 51 |
+
# print(image_buffer.getvalue())
|
| 52 |
+
|
| 53 |
+
pred= predict(image_buffer=image_buffer)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
return jsonify(pred)
|
| 57 |
+
|
| 58 |
+
except (IOError, OSError, ValueError) as e:
|
| 59 |
+
return jsonify(json.dumps({'error': "Server Error "+ str(e)})), 500
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
else:
|
| 63 |
+
return jsonify(json.dumps({'error': "Invalid file format. Please upload a JPEG, or JPG image."})), 500
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
if 'file-input-64' in request.form:
|
| 67 |
+
try:
|
| 68 |
+
# Decode the base64 data (replace with your processing logic)
|
| 69 |
+
base64_data = request.form['file-input-64']
|
| 70 |
+
|
| 71 |
+
image_buffer= io.BytesIO(base64.b64decode(base64_data))
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
try:
|
| 75 |
+
image_buffer.seek(0)
|
| 76 |
+
|
| 77 |
+
# Use Pillow to open the image data from BytesIO
|
| 78 |
+
image = Image.open(image_buffer)
|
| 79 |
+
|
| 80 |
+
# Save the image as a JPEG with appropriate quality (adjust quality as needed)
|
| 81 |
+
# image.save("canvas.jpeg", quality=90, format="JPEG")
|
| 82 |
+
image_buffer.seek(0)
|
| 83 |
+
|
| 84 |
+
pred= predict(image_buffer=image_buffer)
|
| 85 |
+
return jsonify(pred)
|
| 86 |
+
|
| 87 |
+
except (IOError, OSError, ValueError) as e:
|
| 88 |
+
return jsonify(json.dumps({'error': "Khali Na Patha"})), 500
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
except Exception as e:
|
| 93 |
+
return jsonify(json.dumps({'error': str(e)})), 500
|
| 94 |
+
|
| 95 |
+
if __name__ == '__main__':
|
| 96 |
+
app.run(debug=True,reload=True)
|
canvas.jpeg
ADDED
|
dockerfile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.9
|
| 2 |
+
|
| 3 |
+
WORKDIR /code
|
| 4 |
+
|
| 5 |
+
COPY ./requirements.txt /code/requirements.txt
|
| 6 |
+
|
| 7 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 8 |
+
|
| 9 |
+
COPY . .
|
| 10 |
+
|
| 11 |
+
CMD ["gunicorn", "-b", "0.0.0.0:7860","main:app"]
|
model.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
from io import BytesIO
|
| 3 |
+
import cv2
|
| 4 |
+
from captum.attr import IntegratedGradients
|
| 5 |
+
from captum.attr import visualization as viz
|
| 6 |
+
import io
|
| 7 |
+
import torch
|
| 8 |
+
import torchvision
|
| 9 |
+
import torch.nn as nn
|
| 10 |
+
from torchvision import transforms,models
|
| 11 |
+
from torchvision.transforms import v2
|
| 12 |
+
import torch.nn.functional as F
|
| 13 |
+
torchvision.disable_beta_transforms_warning()
|
| 14 |
+
import json
|
| 15 |
+
import base64
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
index_to_target = {
|
| 19 |
+
0: 'अ', 1: 'अं', 2: 'अः', 3: 'आ', 4: 'इ', 5: 'ई', 6: 'उ', 7: 'ऊ', 8: 'ए', 9: 'ऐ', 10: 'ओ', 11: 'औ', 12: 'क', 13: 'क्ष', 14: 'ख', 15: 'ग', 16: 'घ', 17: 'ङ', 18: 'च', 19: 'छ', 20: 'ज', 21: 'ज्ञ', 22: 'झ', 23: 'ञ', 24: 'ट', 25: 'ठ', 26: 'ड', 27: 'ढ', 28: 'ण', 29: 'त', 30: 'त्र', 31: 'थ', 32: 'द', 33: 'ध', 34: 'न', 35: 'प', 36: 'फ', 37: 'ब', 38: 'भ', 39: 'म', 40: 'य', 41: 'र', 42: 'ल', 43: 'व', 44: 'श', 45: 'ष', 46: 'स', 47: 'ह', 48: '०', 49: '१', 50: '२', 51: '३', 52: '४', 53: '५', 54: '६', 55: '७', 56: '८', 57: '९'}
|
| 20 |
+
target_to_index = {value:key for key,value in index_to_target.items()}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
device = 'cuda:0' if torch.cuda.is_available() else "cpu"
|
| 24 |
+
|
| 25 |
+
img_transforms = v2.Compose([
|
| 26 |
+
transforms.ToTensor(),
|
| 27 |
+
v2.ToDtype(torch.float32),
|
| 28 |
+
v2.Normalize((0.5,),(0.5,))
|
| 29 |
+
])
|
| 30 |
+
|
| 31 |
+
#
|
| 32 |
+
def crop_characters(img) -> np.array:
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
blur_img =cv2.GaussianBlur(img,(5,5),3)
|
| 36 |
+
gray = cv2.cvtColor(blur_img,cv2.COLOR_BGR2GRAY)
|
| 37 |
+
|
| 38 |
+
# bin_img= cv2.threshold(gray,0,255,cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
|
| 39 |
+
|
| 40 |
+
thres_value,thresh_img= cv2.threshold(gray,0,255,cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
|
| 41 |
+
|
| 42 |
+
contours, _ = cv2.findContours(thresh_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 43 |
+
|
| 44 |
+
# cv2.drawContours(img,contours,-1,(0,255,0),2)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
bounding_boxes = []
|
| 48 |
+
for contour in contours:
|
| 49 |
+
area = cv2.contourArea(contour)
|
| 50 |
+
|
| 51 |
+
#neglecting very small contours which are actually noise
|
| 52 |
+
if area<200:
|
| 53 |
+
continue
|
| 54 |
+
|
| 55 |
+
# Get the bounding box coordinates
|
| 56 |
+
x, y, w, h = cv2.boundingRect(contour)
|
| 57 |
+
|
| 58 |
+
# Draw rectangle around contour
|
| 59 |
+
# cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0),1)
|
| 60 |
+
|
| 61 |
+
# Store bounding box coordinates
|
| 62 |
+
bounding_boxes.append((x, y, x + w, y + h))
|
| 63 |
+
|
| 64 |
+
# Calculate the minimum bounding rectangle that encloses all the smaller bounding rectangles
|
| 65 |
+
x_min = min(box[0] for box in bounding_boxes)
|
| 66 |
+
y_min = min(box[1] for box in bounding_boxes)
|
| 67 |
+
x_max = max(box[2] for box in bounding_boxes)
|
| 68 |
+
y_max = max(box[3] for box in bounding_boxes)
|
| 69 |
+
|
| 70 |
+
padding_left=3
|
| 71 |
+
padding_right =3
|
| 72 |
+
padding_bottom =3
|
| 73 |
+
padding_top =3
|
| 74 |
+
x_min -= padding_left
|
| 75 |
+
y_min -= padding_bottom
|
| 76 |
+
x_max += padding_top
|
| 77 |
+
y_max += padding_right
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
cropped_img = img[y_min:y_max, x_min:x_max]
|
| 81 |
+
return cropped_img
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def predict(image_buffer:BytesIO)->str:
|
| 87 |
+
device = 'cuda:0' if torch.cuda.is_available() else "cpu"
|
| 88 |
+
|
| 89 |
+
model_path ="res97_state.pth"
|
| 90 |
+
|
| 91 |
+
model = models.resnet101(weights=None).to(device)
|
| 92 |
+
num_classes = 58
|
| 93 |
+
model.fc = nn.Linear(model.fc.in_features, num_classes).to(device)
|
| 94 |
+
model.load_state_dict(torch.load(model_path,map_location=device))
|
| 95 |
+
|
| 96 |
+
image_buffer.seek(0)
|
| 97 |
+
|
| 98 |
+
img= cv2.imdecode(np.frombuffer(image_buffer.read(),np.uint8),-1)
|
| 99 |
+
|
| 100 |
+
if img is None:
|
| 101 |
+
raise RuntimeError("Failed to decode image")
|
| 102 |
+
|
| 103 |
+
img = crop_characters(img)
|
| 104 |
+
|
| 105 |
+
img = cv2.resize(img, (28, 28), interpolation=cv2.INTER_AREA)
|
| 106 |
+
|
| 107 |
+
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # Convert to grayscale
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
img_bin = cv2.adaptiveThreshold(img_gray,255,cv2.ADAPTIVE_THRESH_MEAN_C,cv2.THRESH_BINARY,11,6)
|
| 111 |
+
thres_rgb= cv2.cvtColor(img_bin,cv2.COLOR_GRAY2BGR)
|
| 112 |
+
|
| 113 |
+
model.eval()
|
| 114 |
+
|
| 115 |
+
transformed_img = img_transforms(thres_rgb).unsqueeze(dim=0)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
with torch.inference_mode():
|
| 120 |
+
transformed_img = transformed_img.to(device)
|
| 121 |
+
outputs = model(transformed_img)
|
| 122 |
+
_,predicted_index = torch.max(outputs.data,1) #returns max value and index
|
| 123 |
+
probabilities = F.softmax(outputs,1)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def attribute_image_features(algorithm,image , **kwargs):
|
| 128 |
+
model.zero_grad()
|
| 129 |
+
tensor_attributions = algorithm.attribute(image,
|
| 130 |
+
target=predicted_index,
|
| 131 |
+
**kwargs
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
return tensor_attributions
|
| 135 |
+
|
| 136 |
+
ig = IntegratedGradients(model)
|
| 137 |
+
attr_ig, delta = attribute_image_features(ig, transformed_img, baselines=transformed_img * 0, return_convergence_delta=True)
|
| 138 |
+
attr_ig = np.transpose(attr_ig.squeeze().cpu().detach().numpy(), (1, 2, 0))
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
original_image = img
|
| 142 |
+
|
| 143 |
+
# org_img,axes1 = viz.visualize_image_attr(None, original_image,
|
| 144 |
+
# method="original_image", title="Original Image",use_pyplot=False)
|
| 145 |
+
|
| 146 |
+
img_attr,axes2= viz.visualize_image_attr(attr_ig, original_image, method="blended_heat_map",sign="all",
|
| 147 |
+
title="Overlayed Integrated Gradients",use_pyplot=False,show_colorbar=True)
|
| 148 |
+
|
| 149 |
+
img_attr_bytes = io.BytesIO()
|
| 150 |
+
img_attr.savefig(img_attr_bytes,format="jpeg")
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
top3,top3index= torch.topk(probabilities,3)
|
| 155 |
+
# print(top3,top3index)
|
| 156 |
+
top3Value= top3.tolist()
|
| 157 |
+
top3Index= top3index.tolist()
|
| 158 |
+
# print(top3Value,top3Index)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
json_data_dict={
|
| 162 |
+
"prob":top3Value[0],
|
| 163 |
+
"item":[ index_to_target[int(item)] for item in top3Index[0]],
|
| 164 |
+
"ig":base64.b64encode(img_attr_bytes.getvalue()).decode('utf-8')
|
| 165 |
+
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
return json.dumps( json_data_dict)
|
| 169 |
+
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask
|
| 2 |
+
opencv-python
|
| 3 |
+
scikit-learn
|
| 4 |
+
numpy
|
| 5 |
+
pandas
|
| 6 |
+
matplotlib
|
| 7 |
+
torch
|
| 8 |
+
torchvision
|
static/banner.png
ADDED
|
static/clear.png
ADDED
|
static/logo.png
ADDED
|
static/script/dialog.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
const canvas = document.getElementById('drawing-board');
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
// Get the 2D rendering context of the canvas
|
| 6 |
+
var ctx = canvas.getContext('2d');
|
| 7 |
+
|
| 8 |
+
// Initialize variables to track pointer movements
|
| 9 |
+
var isDrawing = false;
|
| 10 |
+
var lastX = 0;
|
| 11 |
+
var lastY = 0;
|
| 12 |
+
|
| 13 |
+
// Event listener to track pointer movements and draw lines
|
| 14 |
+
canvas.addEventListener('pointerdown', function (e) {
|
| 15 |
+
isDrawing = true;
|
| 16 |
+
[lastX, lastY] = [e.offsetX, e.offsetY];
|
| 17 |
+
});
|
| 18 |
+
|
| 19 |
+
canvas.addEventListener('pointermove', function (e) {
|
| 20 |
+
if (isDrawing) {
|
| 21 |
+
var x = e.offsetX;
|
| 22 |
+
var y = e.offsetY;
|
| 23 |
+
drawLine(lastX, lastY, x, y);
|
| 24 |
+
lastX = x;
|
| 25 |
+
lastY = y;
|
| 26 |
+
}
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
canvas.addEventListener('pointerup', function () {
|
| 30 |
+
isDrawing = false;
|
| 31 |
+
});
|
| 32 |
+
|
| 33 |
+
canvas.addEventListener('pointerout', function () {
|
| 34 |
+
isDrawing = false;
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
// Function to draw lines on the canvas
|
| 38 |
+
function drawLine(startX, startY, endX, endY) {
|
| 39 |
+
ctx.beginPath();
|
| 40 |
+
ctx.moveTo(startX, startY);
|
| 41 |
+
ctx.lineTo(endX, endY);
|
| 42 |
+
ctx.strokeStyle = '#000'; // Color of the line
|
| 43 |
+
ctx.lineWidth = 2; // Thickness of the line
|
| 44 |
+
ctx.stroke();
|
| 45 |
+
ctx.closePath();
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
function showDialog() {
|
| 51 |
+
|
| 52 |
+
const modal = document.getElementById("dialog-area")
|
| 53 |
+
const overlay = document.getElementById("overlay");
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
const canvas = document.getElementById("drawing-board");
|
| 57 |
+
const ctx = canvas.getContext('2d');
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
ctx.fillStyle = "white";
|
| 62 |
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
| 63 |
+
|
| 64 |
+
modal.style.display = 'flex';
|
| 65 |
+
overlay.style.display = 'block';
|
| 66 |
+
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
function closeDialog() {
|
| 72 |
+
const modal = document.getElementById("dialog-area");
|
| 73 |
+
const overlay = document.getElementById("overlay");
|
| 74 |
+
|
| 75 |
+
const canvas = document.getElementById("drawing-board");
|
| 76 |
+
const ctx = canvas.getContext('2d');
|
| 77 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 78 |
+
modal.style.display = 'none';
|
| 79 |
+
overlay.style.display = 'none';
|
| 80 |
+
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
function clearCanvas() {
|
| 85 |
+
ctx.fillStyle = "white";
|
| 86 |
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
| 87 |
+
}
|
static/script/faq.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const faqQuestions = document.querySelectorAll('.faq-question');
|
| 2 |
+
|
| 3 |
+
faqQuestions.forEach(question => {
|
| 4 |
+
question.addEventListener('click', function () {
|
| 5 |
+
const answer = this.nextElementSibling;
|
| 6 |
+
answer.classList.toggle('show');
|
| 7 |
+
// const arrow = this.querySelector('.faq-arrow');
|
| 8 |
+
// arrow.classList.toggle('rotate'); // Simplified class toggle
|
| 9 |
+
});
|
| 10 |
+
});
|
static/script/metrices.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/script/nav.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
function setIdActive(id) {
|
| 5 |
+
const menu = document.getElementById(id);
|
| 6 |
+
|
| 7 |
+
// const navMenu = document.querySelectorAll("#nav-bar a");
|
| 8 |
+
|
| 9 |
+
// navMenu.forEach(item => {
|
| 10 |
+
// item.classList.remove("active");
|
| 11 |
+
// });
|
| 12 |
+
|
| 13 |
+
menu.classList.add("active");
|
| 14 |
+
}
|
static/script/plotly.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
static/script/popup.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const images = [...document.querySelectorAll('.image')];
|
| 2 |
+
|
| 3 |
+
// popup
|
| 4 |
+
|
| 5 |
+
const popup = document.querySelector('.popup');
|
| 6 |
+
const closeBtn = document.querySelector('.close-btn');
|
| 7 |
+
const largeImage = document.querySelector('.large-image');
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
popup.addEventListener('click', (e) => {
|
| 11 |
+
|
| 12 |
+
const bound = largeImage.getBoundingClientRect();
|
| 13 |
+
const x = e.clientX;
|
| 14 |
+
const y = e.clientY;
|
| 15 |
+
|
| 16 |
+
if (!(x >= bound.left && x <= bound.right &&
|
| 17 |
+
y >= bound.top && y <= bound.bottom)) {
|
| 18 |
+
popup.classList.toggle('active');u
|
| 19 |
+
}
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
images.forEach((item, i) => {
|
| 23 |
+
item.addEventListener('click', () => {
|
| 24 |
+
|
| 25 |
+
largeImage.src = item.src;
|
| 26 |
+
popup.classList.toggle('active');
|
| 27 |
+
})
|
| 28 |
+
})
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
closeBtn.addEventListener('click', () => {
|
| 32 |
+
popup.classList.toggle('active');
|
| 33 |
+
})
|
static/script/script.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
const fileInput = document.getElementById('file-input');
|
| 3 |
+
const button = document.getElementById('button');
|
| 4 |
+
const errorMessage = document.getElementById('error-message');
|
| 5 |
+
const container = document.getElementById('container');
|
| 6 |
+
|
| 7 |
+
const imageContainer = document.getElementById('image-container');
|
| 8 |
+
const previewImage = document.getElementById('file-image');
|
| 9 |
+
const resultImage = document.getElementById('result-image');
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
const fileTitle = document.querySelector(".title");
|
| 13 |
+
const predInfo = document.querySelector(".pred-info");
|
| 14 |
+
|
| 15 |
+
const predictedValue = document.querySelector(".pred-info h3 span");
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
const topPred = document.querySelector(".pred-info ul");
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
const sendByCanvas = document.getElementById("canvas-send");
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
function createBlobFromImageData(imageData, mimeType = "image/jpeg") {
|
| 26 |
+
const buffer = new ArrayBuffer(imageData.length);
|
| 27 |
+
const view = new Uint8Array(buffer);
|
| 28 |
+
for (let i = 0; i < imageData.length; i++) {
|
| 29 |
+
view[i] = imageData.charCodeAt(i);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
return new Blob([buffer], { type: mimeType });
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
function initialState() {
|
| 37 |
+
button.textContent = 'Choose file';
|
| 38 |
+
predInfo.style.display = 'none';
|
| 39 |
+
fileTitle.textContent = "Upload your file here";
|
| 40 |
+
imageContainer.style.display = 'none';
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
function displayError(message, timeout = 3000) { // Set default timeout if needed
|
| 44 |
+
errorMessage.textContent = message;
|
| 45 |
+
errorMessage.style.display = 'block';
|
| 46 |
+
initialState();
|
| 47 |
+
setTimeout(() => {
|
| 48 |
+
errorMessage.style.display = 'none';
|
| 49 |
+
}, timeout);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
async function sendImageToServer(image, filename) {
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
// Send the file to the server using Fetch API
|
| 58 |
+
try {
|
| 59 |
+
const formData = new FormData();
|
| 60 |
+
|
| 61 |
+
console.log(image)
|
| 62 |
+
|
| 63 |
+
const base64Data = image.split(',')[1];
|
| 64 |
+
|
| 65 |
+
formData.append('file-input-64', base64Data);
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
// formData.append('file-name', filename);
|
| 71 |
+
|
| 72 |
+
// console.log(`form data ${formData}`);
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
predInfo.style.display = 'none';
|
| 76 |
+
button.textContent = 'Processing....';
|
| 77 |
+
fileTitle.textContent = filename;
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
console.log("Submit button is clicked");
|
| 82 |
+
|
| 83 |
+
const response = await fetch('/upload', {
|
| 84 |
+
method: 'POST',
|
| 85 |
+
body: formData
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
topPred.innerHTML = "";
|
| 89 |
+
console.log('I am here')
|
| 90 |
+
|
| 91 |
+
const jsonData = await response.json();
|
| 92 |
+
|
| 93 |
+
console.log(jsonData)
|
| 94 |
+
console.log(typeof (jsonData))
|
| 95 |
+
const jsonObject = JSON.parse(jsonData);
|
| 96 |
+
|
| 97 |
+
if (!response.ok) {
|
| 98 |
+
throw new Error(`Error uploading file: ${jsonData.error}`);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
resultImage.src = `data:image/jpeg;base64,` + jsonObject["ig"];
|
| 102 |
+
predInfo.style.display = 'flex';
|
| 103 |
+
|
| 104 |
+
predictedValue.textContent = `${jsonObject.item[0]} ( ${(jsonObject.prob[0] * 100).toFixed(3)}%)`;
|
| 105 |
+
|
| 106 |
+
for (let index = 0; index < jsonObject.item.length; index++) {
|
| 107 |
+
const newItem = document.createElement("li");
|
| 108 |
+
newItem.textContent = `${jsonObject.item[index]} ( ${(jsonObject.prob[index] * 100).toFixed(3)}%)`
|
| 109 |
+
topPred.appendChild(newItem);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
// Handle successful upload response (e.g., display message)
|
| 114 |
+
console.log('File uploaded successfully!');
|
| 115 |
+
|
| 116 |
+
} catch (error) {
|
| 117 |
+
// errorMessage.textContent = `Error uploading file: ${error.message}`;
|
| 118 |
+
displayError(error.message);
|
| 119 |
+
} finally {
|
| 120 |
+
// Hide progress message (optional)
|
| 121 |
+
button.textContent = 'Choose file';
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
async function sentImageDataToSeverViaImage(event) {
|
| 126 |
+
|
| 127 |
+
const file = event.target.files[0];
|
| 128 |
+
const allowedExtensions = ['jpeg', 'jpg'];
|
| 129 |
+
|
| 130 |
+
const extension = file.name.split('.').pop().toLowerCase();
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
// console.log(extension);
|
| 134 |
+
|
| 135 |
+
if (!allowedExtensions.includes(extension)) {
|
| 136 |
+
event.target.value = ''; // Clear file selection
|
| 137 |
+
displayError('Invalid file format. Please upload a JPG or JPEG image.')
|
| 138 |
+
} else {
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
const reader = new FileReader();
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
reader.onload = function (event) {
|
| 145 |
+
previewImage.src = event.target.result;
|
| 146 |
+
resultImage.src = '';
|
| 147 |
+
|
| 148 |
+
imageContainer.style.display = 'flex';
|
| 149 |
+
|
| 150 |
+
};
|
| 151 |
+
|
| 152 |
+
reader.readAsDataURL(file); // Read the file and convert it to a data URL
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
// Send the file to the server using Fetch API
|
| 156 |
+
try {
|
| 157 |
+
const formData = new FormData();
|
| 158 |
+
formData.append('file-input', file);
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
button.textContent = 'Processing....';
|
| 163 |
+
fileTitle.textContent = file.name;
|
| 164 |
+
predInfo.style.display = 'none';
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
const response = await fetch('/upload', {
|
| 170 |
+
method: 'POST',
|
| 171 |
+
body: formData
|
| 172 |
+
});
|
| 173 |
+
|
| 174 |
+
if (!response.ok) {
|
| 175 |
+
throw new Error(`Error uploading file: ${response.statusText}`);
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
topPred.innerHTML = "";
|
| 179 |
+
|
| 180 |
+
const jsonData = await response.json();
|
| 181 |
+
|
| 182 |
+
console.log(jsonData)
|
| 183 |
+
console.log(typeof (jsonData))
|
| 184 |
+
const jsonObject = JSON.parse(jsonData);
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
resultImage.src = `data:image/jpeg;base64,` + jsonObject["ig"];
|
| 188 |
+
predInfo.style.display = 'flex';
|
| 189 |
+
|
| 190 |
+
predictedValue.textContent = `${jsonObject.item[0]} ( ${(jsonObject.prob[0] * 100).toFixed(3)}%)`;
|
| 191 |
+
|
| 192 |
+
for (let index = 0; index < jsonObject.item.length; index++) {
|
| 193 |
+
const newItem = document.createElement("li");
|
| 194 |
+
newItem.textContent = `${jsonObject.item[index]} ( ${(jsonObject.prob[index] * 100).toFixed(3)}%)`
|
| 195 |
+
topPred.appendChild(newItem);
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
|
| 199 |
+
// Handle successful upload response (e.g., display message)
|
| 200 |
+
console.log('File uploaded successfully!');
|
| 201 |
+
|
| 202 |
+
} catch (error) {
|
| 203 |
+
errorMessage.textContent = `Error uploading file: ${error.message}`;
|
| 204 |
+
} finally {
|
| 205 |
+
// Hide progress message (optional)
|
| 206 |
+
button.textContent = 'Choose file';
|
| 207 |
+
}
|
| 208 |
+
errorMessage.style.display = 'none'; // Hide the error message
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
async function sentImageDataToSeverViaCanvas() {
|
| 214 |
+
const canvas = document.getElementById('drawing-board'); // Replace with your canvas ID
|
| 215 |
+
var dataURL = canvas.toDataURL("image/jpeg");
|
| 216 |
+
|
| 217 |
+
previewImage.src = dataURL;
|
| 218 |
+
resultImage.src = '';
|
| 219 |
+
|
| 220 |
+
imageContainer.style.display = 'flex';
|
| 221 |
+
|
| 222 |
+
// // Create a dummy link text
|
| 223 |
+
// var a = document.createElement('a');
|
| 224 |
+
// // Set the link to the image so that when clicked, the image begins downloading
|
| 225 |
+
// a.href = dataURL
|
| 226 |
+
// // Specify the image filename
|
| 227 |
+
// a.download = 'canvas-download.jpeg';
|
| 228 |
+
// // Click on the link to set off download
|
| 229 |
+
// a.click();
|
| 230 |
+
|
| 231 |
+
closeDialog();
|
| 232 |
+
|
| 233 |
+
await sendImageToServer(dataURL, 'canvas_image.jpeg');
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
button.addEventListener('click', () => {
|
| 243 |
+
errorMessage.style.display = 'none';
|
| 244 |
+
fileInput.click();
|
| 245 |
+
});
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
sendByCanvas.addEventListener('click', async () => {
|
| 251 |
+
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
sentImageDataToSeverViaCanvas();
|
| 255 |
+
});
|
| 256 |
+
|
| 257 |
+
|
| 258 |
+
fileInput.addEventListener('change', async (event) => {
|
| 259 |
+
sentImageDataToSeverViaImage(event);
|
| 260 |
+
});
|
static/style/about.css
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#my-table {
|
| 2 |
+
width: 100%;
|
| 3 |
+
border-collapse: collapse;
|
| 4 |
+
/* border-radius: 8px; */
|
| 5 |
+
border-radius: 8px;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
.container {
|
| 9 |
+
margin: 2%;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
#table-container {
|
| 13 |
+
border-radius: 12px;
|
| 14 |
+
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
#my-table th,
|
| 18 |
+
#my-table td {
|
| 19 |
+
border: 1px solid #ddd;
|
| 20 |
+
padding: 8px;
|
| 21 |
+
text-align: left;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
tr:nth-child(odd) {
|
| 25 |
+
background: #e6e1e1;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
#my-table th {
|
| 29 |
+
background-color: #a7cefa;
|
| 30 |
+
color: white;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
#table-container button {
|
| 34 |
+
display: inline-block;
|
| 35 |
+
padding: 6px 12px;
|
| 36 |
+
margin: 10px 0;
|
| 37 |
+
cursor: pointer;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
#table-container button:hover {
|
| 41 |
+
/* background-color: #45a049; */
|
| 42 |
+
border-color: #0F79EF;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
#my-table {
|
| 47 |
+
width: 100%;
|
| 48 |
+
border-collapse: collapse;
|
| 49 |
+
border-radius: 8px;
|
| 50 |
+
/* Add border radius for roundish feel */
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
#my-table th,
|
| 54 |
+
#my-table td {
|
| 55 |
+
border: 1px solid #ddd;
|
| 56 |
+
padding: 8px;
|
| 57 |
+
text-align: left;
|
| 58 |
+
color: #333;
|
| 59 |
+
/* Set text color */
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
tr:nth-child(odd) {
|
| 63 |
+
background: #f9f9f9;
|
| 64 |
+
/* Lighter background color for odd rows */
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
#my-table th {
|
| 68 |
+
background-color: #4a90e2;
|
| 69 |
+
/* Blue background for table header */
|
| 70 |
+
color: white;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
#table-container button {
|
| 74 |
+
display: inline-block;
|
| 75 |
+
padding: 6px 12px;
|
| 76 |
+
margin: 10px 0;
|
| 77 |
+
cursor: pointer;
|
| 78 |
+
background-color: #4a90e2;
|
| 79 |
+
/* Blue button background */
|
| 80 |
+
color: white;
|
| 81 |
+
/* Button text color */
|
| 82 |
+
border: 1px solid #4a90e2;
|
| 83 |
+
/* Button border color */
|
| 84 |
+
border-radius: 4px;
|
| 85 |
+
/* Button border radius */
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
#table-container button:hover {
|
| 89 |
+
background-color: #0F79EF;
|
| 90 |
+
/* Darker blue on hover */
|
| 91 |
+
border-color: #0F79EF;
|
| 92 |
+
/* Darker blue for border on hover */
|
| 93 |
+
}
|
static/style/dialog.css
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#dialog-area {
|
| 2 |
+
/* min-width: 50vw;
|
| 3 |
+
max-width: 60vw;
|
| 4 |
+
min-height: fit-content;
|
| 5 |
+
|
| 6 |
+
max-height: 60vh; */
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
/* height: 300px; */
|
| 11 |
+
width: 300px;
|
| 12 |
+
height: 300px;
|
| 13 |
+
position: fixed;
|
| 14 |
+
top: 50%;
|
| 15 |
+
left: 50%;
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
/* padding-bottom: 24px; */
|
| 19 |
+
padding-right: 12px;
|
| 20 |
+
padding-left: 12px;
|
| 21 |
+
|
| 22 |
+
/* border-width: 2px;
|
| 23 |
+
border: 2px solid black; */
|
| 24 |
+
|
| 25 |
+
transform: translate(-50%, -50%);
|
| 26 |
+
display: flex;
|
| 27 |
+
flex-direction: column;
|
| 28 |
+
|
| 29 |
+
background-color: #f2f2f2;
|
| 30 |
+
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
|
| 31 |
+
|
| 32 |
+
z-index: 999;
|
| 33 |
+
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.close {
|
| 37 |
+
font-size: 45px;
|
| 38 |
+
font-weight: 600;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
#canvas-send {
|
| 43 |
+
margin: 0 auto;
|
| 44 |
+
flex: 1;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
#clear-btn {
|
| 48 |
+
height: 30px;
|
| 49 |
+
width: 30px;
|
| 50 |
+
margin-right: 5px;
|
| 51 |
+
|
| 52 |
+
cursor: pointer;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
.drawing-board {
|
| 58 |
+
margin-top: 20px;
|
| 59 |
+
/* padding: 12px;
|
| 60 |
+
padding-left: 30px; */
|
| 61 |
+
margin-bottom: 8px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
#drawing-board {
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
margin: 20px;
|
| 68 |
+
width: 100%;
|
| 69 |
+
/* height: 100%; */
|
| 70 |
+
|
| 71 |
+
flex: 1;
|
| 72 |
+
margin: 0 auto;
|
| 73 |
+
background-color: #f2f2f2;
|
| 74 |
+
|
| 75 |
+
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
|
| 76 |
+
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.action-section {
|
| 80 |
+
|
| 81 |
+
margin-top: 10px;
|
| 82 |
+
display: flex;
|
| 83 |
+
/* height: 50px; */
|
| 84 |
+
flex-direction: row;
|
| 85 |
+
/* justify-content: flex-end; */
|
| 86 |
+
height: 35px;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
canvas {
|
| 92 |
+
width: 100%;
|
| 93 |
+
height: 100%;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
#close-cntr {
|
| 100 |
+
display: flex;
|
| 101 |
+
justify-content: start;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
#close-cntr span:hover {
|
| 106 |
+
cursor: pointer;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
#overlay {
|
| 111 |
+
|
| 112 |
+
position: fixed;
|
| 113 |
+
top: 0;
|
| 114 |
+
left: 0;
|
| 115 |
+
width: 100%;
|
| 116 |
+
height: 100%;
|
| 117 |
+
background-color: rgba(0, 0, 0, 0.5);
|
| 118 |
+
z-index: 998;
|
| 119 |
+
|
| 120 |
+
display: none;
|
| 121 |
+
}
|
static/style/help.css
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.container {
|
| 2 |
+
margin: 2%;
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
.faq {
|
| 7 |
+
margin-bottom: 20px;
|
| 8 |
+
position: relative;
|
| 9 |
+
/* Required for arrow positioning */
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
.faq-question {
|
| 13 |
+
cursor: pointer;
|
| 14 |
+
font-weight: bold;
|
| 15 |
+
padding: 10px 15px;
|
| 16 |
+
border-bottom: 1px solid #ddd;
|
| 17 |
+
position: relative;
|
| 18 |
+
/* Required for arrow positioning */
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.faq-answer {
|
| 22 |
+
padding: 10px 15px;
|
| 23 |
+
display: none;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.faq-answer.show {
|
| 27 |
+
display: block;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.faq-arrow {
|
| 31 |
+
width: 0;
|
| 32 |
+
height: 0;
|
| 33 |
+
border-left: 5px solid transparent;
|
| 34 |
+
border-right: 5px solid transparent;
|
| 35 |
+
border-bottom: 8px solid #ddd;
|
| 36 |
+
position: absolute;
|
| 37 |
+
/* Position the arrow */
|
| 38 |
+
right: 10px;
|
| 39 |
+
/* Place it on the right edge */
|
| 40 |
+
top: 50%;
|
| 41 |
+
transform: translateY(-50%);
|
| 42 |
+
/* Vertical centering */
|
| 43 |
+
transition: transform 0.3s ease-in-out;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.faq-answer.show .faq-arrow {
|
| 47 |
+
transform: translateY(-50%) rotate(180deg);
|
| 48 |
+
/* Rotate on expand */
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
|
static/style/home.css
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
text-align: center;
|
| 3 |
+
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
#banner {
|
| 7 |
+
/* Set image to full width and height */
|
| 8 |
+
width: 100%;
|
| 9 |
+
height: 25vh;
|
| 10 |
+
|
| 11 |
+
/* Maintain image aspect ratio and avoid distortion */
|
| 12 |
+
object-fit: contain;
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2);
|
| 16 |
+
|
| 17 |
+
/* Optional: Center the image vertically */
|
| 18 |
+
/* position: absolute; */
|
| 19 |
+
/* top: 50%; */
|
| 20 |
+
/* transform: translateY(-50%); */
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
#blank-area {
|
| 24 |
+
height: 25vh;
|
| 25 |
+
width: 100%;
|
| 26 |
+
background-color: #252525;
|
| 27 |
+
/* margin-top: 20vh; */
|
| 28 |
+
|
| 29 |
+
/* background-color: rgb(102, 91, 91); */
|
| 30 |
+
/* background-image: url("./banner.png"); */
|
| 31 |
+
/* background-position: -20px; */
|
| 32 |
+
margin-bottom: 50px;
|
| 33 |
+
/* background-repeat: no-repeat;
|
| 34 |
+
background-size: contain; */
|
| 35 |
+
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
#container {
|
| 39 |
+
/* width: 400px; */
|
| 40 |
+
height: 20vh;
|
| 41 |
+
margin: 0 auto;
|
| 42 |
+
|
| 43 |
+
padding: 20px;
|
| 44 |
+
border-radius: 5px;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.title {
|
| 48 |
+
font-size: 24px;
|
| 49 |
+
margin-bottom: 10px;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.button {
|
| 53 |
+
background-color: red;
|
| 54 |
+
color: white;
|
| 55 |
+
padding: 10px 20px;
|
| 56 |
+
border: none;
|
| 57 |
+
border-radius: 5px;
|
| 58 |
+
cursor: pointer;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.file-input {
|
| 62 |
+
display: none;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
#image-container {
|
| 66 |
+
display: none;
|
| 67 |
+
text-align: center;
|
| 68 |
+
justify-content: center;
|
| 69 |
+
|
| 70 |
+
flex-wrap: wrap;
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
#image-container div {
|
| 76 |
+
width: 200px;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
#file-image {
|
| 80 |
+
max-width: 200px;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
#result-image {
|
| 86 |
+
max-width: 200px;
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
transition: transform 0.5s ease-in-out;
|
| 90 |
+
}
|
static/style/info-panel.css
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.info-panel {
|
| 2 |
+
width: 70%;
|
| 3 |
+
padding: 20px;
|
| 4 |
+
background-color: white;
|
| 5 |
+
border-radius: 5px;
|
| 6 |
+
|
| 7 |
+
margin: 0 auto;
|
| 8 |
+
margin-bottom: 20px;
|
| 9 |
+
|
| 10 |
+
display: flex;
|
| 11 |
+
flex-wrap: wrap;
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.info-panel .pred-info {
|
| 18 |
+
display: flex;
|
| 19 |
+
flex-direction: column;
|
| 20 |
+
text-align: left;
|
| 21 |
+
flex: 1;
|
| 22 |
+
|
| 23 |
+
display: none;
|
| 24 |
+
|
| 25 |
+
transition: display 1s ease-in-out;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
.info-panel .img-info {
|
| 30 |
+
flex: 2;
|
| 31 |
+
padding-left: 12px;
|
| 32 |
+
|
| 33 |
+
text-align: left;
|
| 34 |
+
|
| 35 |
+
transition: width 1s ease-out;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
.info-panel ul {
|
| 39 |
+
list-style: none;
|
| 40 |
+
padding: 0;
|
| 41 |
+
margin: 0;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.bar {
|
| 45 |
+
height: 70px;
|
| 46 |
+
width: 5px;
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
background-color: red;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
.img-info label {
|
| 54 |
+
display: flex;
|
| 55 |
+
justify-content: flex-end;
|
| 56 |
+
/* Align label content to end */
|
| 57 |
+
}
|
static/style/navbar.css
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#nav-bar {
|
| 2 |
+
/* padding-top: 8px; */
|
| 3 |
+
padding-left: 8px;
|
| 4 |
+
/* padding-bottom: 50px; */
|
| 5 |
+
padding-right: 8px;
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
background-color: white;
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
display: flex;
|
| 12 |
+
align-items: center;
|
| 13 |
+
/* Ensure vertical alignment */
|
| 14 |
+
justify-content: space-between;
|
| 15 |
+
|
| 16 |
+
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
|
| 17 |
+
|
| 18 |
+
/* Distribute evenly */
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
#nav-bar img {
|
| 23 |
+
max-height: 75px;
|
| 24 |
+
/* Adjust as needed */
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
#nav-bar a {
|
| 28 |
+
text-decoration: none;
|
| 29 |
+
color: inherit;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.nav-area {
|
| 33 |
+
display: flex;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.logo-area {
|
| 37 |
+
display: flex;
|
| 38 |
+
flex: 1;
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
display: flex;
|
| 43 |
+
align-items: center;
|
| 44 |
+
/* Optional, depending on desired alignment */
|
| 45 |
+
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.logo-area span {
|
| 49 |
+
padding-bottom: 0;
|
| 50 |
+
/* padding-top: 12px; */
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
#nav-bar span {
|
| 54 |
+
padding: 12px;
|
| 55 |
+
font-weight: bold;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
#nav-bar span:hover {
|
| 59 |
+
/* background-color: rgb(180, 173, 173); */
|
| 60 |
+
justify-content: center;
|
| 61 |
+
cursor: pointer;
|
| 62 |
+
color: #0F79EF;
|
| 63 |
+
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
#nav-bar a.active {
|
| 67 |
+
color: #0F79EF;
|
| 68 |
+
/* text-decoration: underline;
|
| 69 |
+
text-decoration-color: red;
|
| 70 |
+
text-decoration-thickness: 4px; */
|
| 71 |
+
}
|
static/style/popup.css
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.popup {
|
| 2 |
+
display: none;
|
| 3 |
+
/* Initially hide the modal */
|
| 4 |
+
position: fixed;
|
| 5 |
+
left: 0;
|
| 6 |
+
top: 0;
|
| 7 |
+
width: 100%;
|
| 8 |
+
height: 100%;
|
| 9 |
+
background-color: rgba(0, 0, 0, 0.7);
|
| 10 |
+
/* Transparent background with slight opacity */
|
| 11 |
+
text-align: center;
|
| 12 |
+
z-index: 10;
|
| 13 |
+
/* Ensure modal stays above other content */
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
.popup.active {
|
| 17 |
+
display: block;
|
| 18 |
+
/* Show the modal when the 'active' class is added */
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.top-bar {
|
| 22 |
+
position: absolute;
|
| 23 |
+
top: 10px;
|
| 24 |
+
right: 10px;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.close-btn {
|
| 28 |
+
color: red;
|
| 29 |
+
font-size: 36px;
|
| 30 |
+
font-weight: bold;
|
| 31 |
+
cursor: pointer;
|
| 32 |
+
transition: 0.3s ease;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.close-btn:hover {
|
| 36 |
+
color: #f1f1f1;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
.large-image {
|
| 40 |
+
max-width: 100%;
|
| 41 |
+
max-height: 100%;
|
| 42 |
+
margin: auto;
|
| 43 |
+
margin-top: 12px;
|
| 44 |
+
margin-bottom: 12px;
|
| 45 |
+
display: block;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.image {
|
| 49 |
+
cursor: pointer;
|
| 50 |
+
}
|
static/style/style.css
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
font-family: sans-serif;
|
| 3 |
+
margin: 0;
|
| 4 |
+
padding: 0;
|
| 5 |
+
/* padding: 20px; */
|
| 6 |
+
background-color: #f2f2f2;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
#error-message {
|
| 11 |
+
color: white;
|
| 12 |
+
background-color: red;
|
| 13 |
+
padding: 30px;
|
| 14 |
+
border-radius: 5px;
|
| 15 |
+
font-size: 14px;
|
| 16 |
+
display: none;
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
font: size 32px;
|
| 20 |
+
font-weight: bold;
|
| 21 |
+
|
| 22 |
+
position: fixed;
|
| 23 |
+
|
| 24 |
+
top: 90%;
|
| 25 |
+
left: 81%;
|
| 26 |
+
transform: translate(-50%, -50%);
|
| 27 |
+
|
| 28 |
+
/* Center the message horizontally and vertically */
|
| 29 |
+
width: 400px;
|
| 30 |
+
/* Specify desired width */
|
| 31 |
+
text-align: center;
|
| 32 |
+
z-index: 999;
|
| 33 |
+
/* Ensure the message is on top of other elements */
|
| 34 |
+
animation: fade-in 0.5s ease-in-out;
|
| 35 |
+
/* Add a smooth fade-in animation */
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
@keyframes fade-in {
|
| 39 |
+
from {
|
| 40 |
+
opacity: 0;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
to {
|
| 44 |
+
opacity: 1;
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
#error-message.fade-out {
|
| 49 |
+
animation: fade-out 0.5s ease-in-out backwards;
|
| 50 |
+
/* Add a smooth fade-out animation */
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
@keyframes fade-out {
|
| 54 |
+
from {
|
| 55 |
+
opacity: 1;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
to {
|
| 59 |
+
opacity: 0;
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
|
templates/about.html
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>About</title>
|
| 8 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style/about.css') }}">
|
| 9 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style/navbar.css') }}">
|
| 10 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
|
| 11 |
+
|
| 12 |
+
</head>
|
| 13 |
+
|
| 14 |
+
<body>
|
| 15 |
+
|
| 16 |
+
{% extends 'layout.html' %}
|
| 17 |
+
|
| 18 |
+
{% block content %}
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
<div class="container">
|
| 22 |
+
|
| 23 |
+
<h1>Developing an Interpretable Nepali Handwritten Character Recognizer</h1>
|
| 24 |
+
|
| 25 |
+
This project was driven by the desire to create a robust and informative character recognition system for the
|
| 26 |
+
Nepali
|
| 27 |
+
language. We believe that understanding how models make decisions is crucial for building trust and
|
| 28 |
+
transparency,
|
| 29 |
+
especially in culturally significant domains like handwriting recognition.
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
<h3>Technology Stack</h3>
|
| 33 |
+
<ul>
|
| 34 |
+
<li>
|
| 35 |
+
Programming language: Python
|
| 36 |
+
</li>
|
| 37 |
+
<li>
|
| 38 |
+
Deep learning libraries: PyTorch
|
| 39 |
+
</li>
|
| 40 |
+
<li>
|
| 41 |
+
Visualization tools:Matplotlib
|
| 42 |
+
</li>
|
| 43 |
+
<li>
|
| 44 |
+
Web Stack: FlaskI
|
| 45 |
+
</li>
|
| 46 |
+
</ul>
|
| 47 |
+
|
| 48 |
+
<!-- <h3>Future Plans</h3>
|
| 49 |
+
|
| 50 |
+
- Expanding character set recognition
|
| 51 |
+
- Enhancing interpretability methods
|
| 52 |
+
- Integrating with educational or assistive technologies
|
| 53 |
+
|
| 54 |
+
We are committed to advancing this project and its capabilities. We welcome your feedback and collaboration! -->
|
| 55 |
+
|
| 56 |
+
<h2>Metrices</h2>
|
| 57 |
+
|
| 58 |
+
<div id="confusion_matrix_cnt" style="width: 90%;"></div>
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
<h3>Classification Report</h3>
|
| 62 |
+
|
| 63 |
+
<div id="table-container">
|
| 64 |
+
<table id="my-table">
|
| 65 |
+
<thead>
|
| 66 |
+
<tr>
|
| 67 |
+
<th>Class</th>
|
| 68 |
+
<th>Precision</th>
|
| 69 |
+
<th>Recall</th>
|
| 70 |
+
<th>F1 score</th>
|
| 71 |
+
|
| 72 |
+
</tr>
|
| 73 |
+
</thead>
|
| 74 |
+
<tbody>
|
| 75 |
+
<!-- Table rows will be added here -->
|
| 76 |
+
</tbody>
|
| 77 |
+
</table>
|
| 78 |
+
|
| 79 |
+
<!-- Pagination buttons -->
|
| 80 |
+
<button id="prev-btn">Previous</button>
|
| 81 |
+
<span id="page-info"></span>
|
| 82 |
+
<button id="next-btn">Next</button>
|
| 83 |
+
</div>
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
<script src="{{ url_for('static', filename='script/plotly.js') }}"></script>
|
| 89 |
+
<script src="{{ url_for('static', filename='script/metrices.js') }}"></script>
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<script>
|
| 96 |
+
setIdActive("about-men");
|
| 97 |
+
</script>
|
| 98 |
+
{% endblock %}
|
| 99 |
+
|
| 100 |
+
</body>
|
| 101 |
+
|
| 102 |
+
</html>
|
templates/components/navbar.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div id="nav-bar">
|
| 2 |
+
<div class="logo-area">
|
| 3 |
+
<a href="/"><img src="{{ url_for('static', filename='logo.png') }}"></a>
|
| 4 |
+
<a href="/help" id="help-men"><span>Help</span></a>
|
| 5 |
+
|
| 6 |
+
</div>
|
| 7 |
+
<div class="nav-area">
|
| 8 |
+
<a href="/" id="home-men"><span>Home</span></a>
|
| 9 |
+
<a href="/about" id="about-men"> <span>About</span></a>
|
| 10 |
+
</div>
|
| 11 |
+
|
| 12 |
+
<script src="{{ url_for('static', filename='script/nav.js') }}"></script>
|
| 13 |
+
</div>
|
templates/help.html
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Help</title>
|
| 8 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
|
| 9 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style/navbar.css') }}">
|
| 10 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style/help.css') }}">
|
| 11 |
+
</head>
|
| 12 |
+
|
| 13 |
+
<body>
|
| 14 |
+
{% extends 'layout.html' %}
|
| 15 |
+
|
| 16 |
+
{% block content %}
|
| 17 |
+
|
| 18 |
+
<div class="container">
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
<h1>Getting Started</h1>
|
| 22 |
+
|
| 23 |
+
<li> Upload a clear image of a handwritten Nepali character in supported formats (e.g., JPG, JPEG).</li>
|
| 24 |
+
<li> The recognized character will be displayed, along with a visualization of the pixels that influenced the
|
| 25 |
+
prediction.</li>
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
<h3>FAQ</h3>
|
| 31 |
+
|
| 32 |
+
<div class="faq">
|
| 33 |
+
<h3 class="faq-question">What character sets are supported?</h3>
|
| 34 |
+
<div class="faq-answer">
|
| 35 |
+
Currently, the model recognizes all basic Nepali characters (क्ष, त्र,ज्ञ,etc.).
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
<div class="faq">
|
| 39 |
+
<h3 class="faq-question">How accurate is the recognition?</h3>
|
| 40 |
+
<div class="faq-answer">
|
| 41 |
+
The accuracy rate is 97.5% on a standard test dataset.
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
<div class="faq">
|
| 45 |
+
<h3 class="faq-question">Will you add support for more features?</h3>
|
| 46 |
+
<div class="faq-answer">
|
| 47 |
+
We are actively working on improving the model and adding new
|
| 48 |
+
features. Stay tuned for updates!
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
</div>
|
| 54 |
+
<script src="{{ url_for('static', filename='script/faq.js') }}"></script>
|
| 55 |
+
<script>
|
| 56 |
+
setIdActive("help-men");
|
| 57 |
+
</script>
|
| 58 |
+
{% endblock %}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
</body>
|
| 62 |
+
|
| 63 |
+
</html>
|
templates/index.html
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>अक्षर</title>
|
| 8 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
|
| 9 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style/navbar.css') }}">
|
| 10 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style/home.css') }}">
|
| 11 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style/info-panel.css') }}">
|
| 12 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style/dialog.css') }}">
|
| 13 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style/popup.css') }}">
|
| 14 |
+
<link rel="icon" href="{{ url_for('static', filename='logo.png') }}" type="image/x-icon">
|
| 15 |
+
</head>
|
| 16 |
+
|
| 17 |
+
<body>
|
| 18 |
+
|
| 19 |
+
{% extends 'layout.html' %}
|
| 20 |
+
|
| 21 |
+
{% block content %}
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
<div id="blank-area">
|
| 25 |
+
<img id="banner" src="{{ url_for('static', filename='banner.png') }}">
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
<!-- <div id="container">
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
</div> -->
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
<div class="popup">
|
| 38 |
+
<div class="top-bar">
|
| 39 |
+
<span class="close-btn">×</span>
|
| 40 |
+
</div>
|
| 41 |
+
<img src="" class="large-image" alt="">
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
<div class="info-panel">
|
| 48 |
+
<div class="pred-info">
|
| 49 |
+
<h3>Predicted Label: <span></span></h3>
|
| 50 |
+
<h5>Top 3 Predictions:</h5>
|
| 51 |
+
<ul>
|
| 52 |
+
</ul>
|
| 53 |
+
</div>
|
| 54 |
+
|
| 55 |
+
<div class="bar"></div>
|
| 56 |
+
|
| 57 |
+
<div class="img-info">
|
| 58 |
+
<h3>Photos</h3>
|
| 59 |
+
<h6 class="title">Upload your file here</h6>
|
| 60 |
+
<div id="image-container">
|
| 61 |
+
<img id='file-image'>
|
| 62 |
+
<img id='result-image' class="image">
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
<div style="display: flex;flex-direction: row;justify-content: flex-end;">
|
| 67 |
+
<label style="margin-right: 12px;">
|
| 68 |
+
<button class="button" id="try-btn" style="background-color: #1363EF;" onclick="showDialog()">Try
|
| 69 |
+
Yourself</button>
|
| 70 |
+
</label>
|
| 71 |
+
|
| 72 |
+
<label for="file-input">
|
| 73 |
+
<input type="file" id="file-input" class="file-input" accept="image/jpeg,image/jpg">
|
| 74 |
+
<button class="button" id="button">Choose file </button>
|
| 75 |
+
</label>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
<div id="overlay"></div>
|
| 83 |
+
<div id="dialog-area" style="display: none;">
|
| 84 |
+
|
| 85 |
+
<div id="close-cntr"> <span
|
| 86 |
+
style="padding-left: 90px; padding-top: 15px; font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; font-size: 18px;font-weight: bold;">
|
| 87 |
+
PLAYGROUND</span>
|
| 88 |
+
<div style="flex: 1;"></div><span class="close" onclick="closeDialog()">×</span>
|
| 89 |
+
</div>
|
| 90 |
+
|
| 91 |
+
<div class="drawing-board">
|
| 92 |
+
<canvas id="drawing-board"></canvas>
|
| 93 |
+
</div>
|
| 94 |
+
<span
|
| 95 |
+
style="font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; font-size: 12px;font-weight: 600; color: grey;">NOTE:
|
| 96 |
+
Make sure to cover the drawing area</span>
|
| 97 |
+
<div class="action-section">
|
| 98 |
+
<img src="{{ url_for('static', filename='clear.png') }}" id="clear-btn" onclick="clearCanvas()">
|
| 99 |
+
<button class="button" style="background-color: #1363EF;" id="canvas-send">Submit</button>
|
| 100 |
+
</div>
|
| 101 |
+
|
| 102 |
+
</div>
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
<div id="error-message"></div>
|
| 108 |
+
<script src="{{ url_for('static', filename='script/script.js') }}"></script>
|
| 109 |
+
<script src="{{ url_for('static', filename='script/dialog.js') }}"></script>
|
| 110 |
+
<script src="{{ url_for('static', filename='script/popup.js') }}"></script>
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
<script>
|
| 114 |
+
setIdActive("home-men");
|
| 115 |
+
</script>
|
| 116 |
+
|
| 117 |
+
{% endblock %}
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
</body>
|
| 123 |
+
|
| 124 |
+
</html>
|
templates/layout.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html>
|
| 3 |
+
<head>
|
| 4 |
+
<title>My Website</title>
|
| 5 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
|
| 6 |
+
</head>
|
| 7 |
+
<body>
|
| 8 |
+
{% include 'components/navbar.html' %}
|
| 9 |
+
<main>
|
| 10 |
+
{% block content %}{% endblock %}
|
| 11 |
+
</main>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|