Sunil Surendra Singh
commited on
Commit
β’
c803eb6
1
Parent(s):
9b9203a
added gradio client code
Browse files- .gitattributes +1 -0
- .gitignore +1 -0
- README.md +21 -4
- assets/app-example.png +3 -0
- assets/examples/1.png +3 -0
- assets/examples/10.png +3 -0
- assets/examples/2.png +3 -0
- assets/examples/3.png +3 -0
- assets/examples/4.png +3 -0
- assets/examples/5.png +3 -0
- assets/examples/6.png +3 -0
- assets/examples/7.png +3 -0
- assets/examples/8.png +3 -0
- assets/examples/9.png +3 -0
- requirements.txt +5 -1
- src/app_config.py +32 -2
- src/client.py +167 -1
- src/mongo_utils.py +57 -0
- src/server.py +25 -32
- src/style.css +11 -0
.gitattributes
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
.gitignore
CHANGED
@@ -2,6 +2,7 @@
|
|
2 |
.env
|
3 |
notebook*/
|
4 |
venv*/
|
|
|
5 |
*.excalidraw
|
6 |
.vscode*
|
7 |
.vscode*/
|
|
|
2 |
.env
|
3 |
notebook*/
|
4 |
venv*/
|
5 |
+
pyvenv.cfg
|
6 |
*.excalidraw
|
7 |
.vscode*
|
8 |
.vscode*/
|
README.md
CHANGED
@@ -2,7 +2,10 @@
|
|
2 |
> Where Math Meets Marvel with AI Wizardry for Primary School Prowess! π§βπβ¨
|
3 |
|
4 |
## Introduction
|
5 |
-
GuruZee, your go-to guru for primary school math (for now).
|
|
|
|
|
|
|
6 |
|
7 |
## Objectives
|
8 |
1. Given just an image of primary school math's problem, GuruZee shall produce the
|
@@ -10,8 +13,22 @@ solution and clear explanation of the solution.
|
|
10 |
2. Given the images of a chapter from primary school maths GuruZee will generate the
|
11 |
question paper for various difficulty level based on the content's in the chapter.
|
12 |
|
13 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
[TODO]
|
15 |
|
16 |
-
## How to use
|
17 |
-
[TODO]
|
|
|
2 |
> Where Math Meets Marvel with AI Wizardry for Primary School Prowess! π§βπβ¨
|
3 |
|
4 |
## Introduction
|
5 |
+
GuruZee, your go-to guru for primary school math (for now).
|
6 |
+
Picture this. GuruZee whips up question papers with answers and detailed explanations,
|
7 |
+
all based on those elusive course chapter images. Not just that, it's a math-solving
|
8 |
+
maestro, tackling problems presented as images with ease.
|
9 |
|
10 |
## Objectives
|
11 |
1. Given just an image of primary school math's problem, GuruZee shall produce the
|
|
|
13 |
2. Given the images of a chapter from primary school maths GuruZee will generate the
|
14 |
question paper for various difficulty level based on the content's in the chapter.
|
15 |
|
16 |
+
## How to use
|
17 |
+
Solve a problem
|
18 |
+
---------------
|
19 |
+
- Select/upload an `Image` containing the problem Then hit `GO!` button. Alternatively,
|
20 |
+
just select one of the pre-configured `Example` image from Example section at the
|
21 |
+
bottom. The answer and explanation will appear on the right.
|
22 |
+
|
23 |
+
<img src="https://github.com/sssingh/guruzee/blob/main/assets/aap=eample.png?raw=true" width="1000" height="450"/><br><br>
|
24 |
+
|
25 |
+
Prepare questions and answers
|
26 |
+
-----------------------------
|
27 |
+
WIP (work in progress)
|
28 |
+
- Select/upload an `Image` containing the problem Then hit `GO!` button. Alternatively,
|
29 |
+
just select one the pre-configured `Example` image from Example section in the bottom.
|
30 |
+
The answer and explanation will appear on the right.
|
31 |
+
|
32 |
+
## App Architecture
|
33 |
[TODO]
|
34 |
|
|
|
|
assets/app-example.png
ADDED
Git LFS Details
|
assets/examples/1.png
ADDED
Git LFS Details
|
assets/examples/10.png
ADDED
Git LFS Details
|
assets/examples/2.png
ADDED
Git LFS Details
|
assets/examples/3.png
ADDED
Git LFS Details
|
assets/examples/4.png
ADDED
Git LFS Details
|
assets/examples/5.png
ADDED
Git LFS Details
|
assets/examples/6.png
ADDED
Git LFS Details
|
assets/examples/7.png
ADDED
Git LFS Details
|
assets/examples/8.png
ADDED
Git LFS Details
|
assets/examples/9.png
ADDED
Git LFS Details
|
requirements.txt
CHANGED
@@ -1,2 +1,6 @@
|
|
1 |
fastapi
|
2 |
-
|
|
|
|
|
|
|
|
|
|
1 |
fastapi
|
2 |
+
uvicorn
|
3 |
+
requests
|
4 |
+
pymongo[tls, srv]==4.4.*
|
5 |
+
gradio==3.45.*
|
6 |
+
python-dotenv
|
src/app_config.py
CHANGED
@@ -1,10 +1,40 @@
|
|
|
|
|
|
1 |
from dataclasses import dataclass
|
2 |
|
|
|
|
|
|
|
3 |
|
4 |
@dataclass
|
5 |
class __AppConfig:
|
6 |
-
|
7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
|
9 |
|
10 |
config = __AppConfig()
|
|
|
1 |
+
import os
|
2 |
+
from dotenv import load_dotenv
|
3 |
from dataclasses import dataclass
|
4 |
|
5 |
+
# load environment variables from .env (only available in dev environment)
|
6 |
+
load_dotenv()
|
7 |
+
|
8 |
|
9 |
@dataclass
|
10 |
class __AppConfig:
|
11 |
+
solver_persona = """You an expert primary school maths teacher. Given an
|
12 |
+
image of a primary school maths problem you can analyze the problem and
|
13 |
+
produce a detailed solution with explanation."""
|
14 |
+
teacher_persona = (
|
15 |
+
solver_persona
|
16 |
+
+ """ \nIn addition to this, given a
|
17 |
+
collection images from primary school maths text book you can generate
|
18 |
+
questions and there answers based on the topic shown in text book images,
|
19 |
+
you will provide the detailed answers with explanation"""
|
20 |
+
)
|
21 |
+
solver_instruction = """Analyze the primary school math problem in the the image.
|
22 |
+
Strictly first provide the answer to the problem and then only the solution
|
23 |
+
explanation. Put a newline after each 80 chars"""
|
24 |
+
openai_max_access_count = 200
|
25 |
+
openai_curr_access_count = None
|
26 |
+
mongo_client = None
|
27 |
+
db = "mydb"
|
28 |
+
collection = "guruzee-openai-access-counter"
|
29 |
+
key = "current_count"
|
30 |
+
OPENAI_API_ENDPOINT = os.getenv("OPENAI_API_ENDPOINT")
|
31 |
+
GURUZEE_API_ENDPOINT = os.getenv("GURUZEE_API_ENDPOINT")
|
32 |
+
OPENAI_KEY = os.getenv("OPENAI_KEY")
|
33 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
34 |
+
MONGO_CONN_STR = os.getenv("MONGO_CONN_STR")
|
35 |
+
title = "GuruZee, your go-to guru for primary school math"
|
36 |
+
theme = "freddyaboulton/dracula_revamped"
|
37 |
+
css = "style.css"
|
38 |
|
39 |
|
40 |
config = __AppConfig()
|
src/client.py
CHANGED
@@ -1 +1,167 @@
|
|
1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import base64
|
2 |
+
import requests
|
3 |
+
from app_config import config
|
4 |
+
import gradio as gr
|
5 |
+
import mongo_utils as mongo
|
6 |
+
|
7 |
+
|
8 |
+
def clear():
|
9 |
+
return None, ""
|
10 |
+
|
11 |
+
|
12 |
+
# Function to encode the local image into base64 to be send over HTTP
|
13 |
+
def __encode_image(image_path: str) -> bytes:
|
14 |
+
"""
|
15 |
+
Encodes the passed image to base64 byte-stream.
|
16 |
+
This is required for passing the image over a HTTP get/post call.
|
17 |
+
"""
|
18 |
+
with open(image_path, "rb") as image_file:
|
19 |
+
base64_image = base64.b64encode(image_file.read()).decode("utf-8")
|
20 |
+
return base64_image
|
21 |
+
|
22 |
+
|
23 |
+
def solve(image_path: str):
|
24 |
+
"""
|
25 |
+
Invokes the GuruZee API passing the raw image bytes and returns the response
|
26 |
+
to client
|
27 |
+
"""
|
28 |
+
print(image_path)
|
29 |
+
image_data = __encode_image(image_path=image_path)
|
30 |
+
headers = {"Content-Type": "application/json"}
|
31 |
+
payload = {"data": image_data}
|
32 |
+
response = requests.post(config.GURUZEE_API_ENDPOINT, headers=headers, json=payload)
|
33 |
+
return response.json()
|
34 |
+
|
35 |
+
|
36 |
+
def create_interface():
|
37 |
+
js_enable_darkmode = """() =>
|
38 |
+
{
|
39 |
+
document.querySelector('body').classList.add('dark');
|
40 |
+
}"""
|
41 |
+
js_toggle_darkmode = """() =>
|
42 |
+
{
|
43 |
+
if (document.querySelectorAll('.dark').length) {
|
44 |
+
document.querySelector('body').classList.remove('dark');
|
45 |
+
} else {
|
46 |
+
document.querySelector('body').classList.add('dark');
|
47 |
+
}
|
48 |
+
}"""
|
49 |
+
|
50 |
+
with gr.Blocks(title=config.title, theme=config.theme, css=config.css) as app:
|
51 |
+
# enable darkmode
|
52 |
+
app.load(fn=None, inputs=None, outputs=None, _js=js_enable_darkmode)
|
53 |
+
with gr.Row():
|
54 |
+
darkmode_checkbox = gr.Checkbox(
|
55 |
+
label="Dark Mode", value=True, interactive=True
|
56 |
+
)
|
57 |
+
# toggle darkmode on/off when checkbox is checked/unchecked
|
58 |
+
darkmode_checkbox.change(
|
59 |
+
None, None, None, _js=js_toggle_darkmode, api_name=False
|
60 |
+
)
|
61 |
+
with gr.Row():
|
62 |
+
with gr.Column():
|
63 |
+
gr.Markdown(
|
64 |
+
"""
|
65 |
+
# GuruZee
|
66 |
+
***Where Math Meets Marvel with AI Wizardry for Primary School
|
67 |
+
Prowess! π§βπβ¨***
|
68 |
+
**GuruZee whips up question papers with answers and detailed
|
69 |
+
explanations, all based on those elusive course chapter images.
|
70 |
+
Not just that, it's a math-solving maestro, tackling problems
|
71 |
+
presented as images with ease..
|
72 |
+
<br>
|
73 |
+
Select/upload an `Image` containing the problem Then hit
|
74 |
+
`GO!` button.
|
75 |
+
Alternatively, just select one the pre-configured `Example` image
|
76 |
+
from Example section in the bottom**
|
77 |
+
<br>
|
78 |
+
Visit the [project's repo](https://github.com/sssingh/GuruZee)
|
79 |
+
<br>
|
80 |
+
***Please exercise patience, as the models employed are extensive
|
81 |
+
and may require a few seconds to load. If you encounter an unrelated
|
82 |
+
response, it is likely still loading; wait a moment and try again.***
|
83 |
+
"""
|
84 |
+
)
|
85 |
+
with gr.Column():
|
86 |
+
max_count = gr.Textbox(
|
87 |
+
label="Max allowed OpenAI requests:",
|
88 |
+
value=config.openai_max_access_count,
|
89 |
+
)
|
90 |
+
curr_count = gr.Textbox(
|
91 |
+
label="Used up OpenAI requests:",
|
92 |
+
value=config.openai_curr_access_count,
|
93 |
+
)
|
94 |
+
available_count = gr.Textbox(
|
95 |
+
label="Available OpenAI requests:",
|
96 |
+
value=config.openai_max_access_count
|
97 |
+
- config.openai_curr_access_count,
|
98 |
+
)
|
99 |
+
with gr.Row():
|
100 |
+
with gr.Column():
|
101 |
+
image = gr.Image(
|
102 |
+
type="filepath",
|
103 |
+
)
|
104 |
+
with gr.Row():
|
105 |
+
submit_button = gr.Button(value="GO!", elem_classes="orange-button")
|
106 |
+
clear_button = gr.ClearButton(elem_classes="gray-button")
|
107 |
+
with gr.Column():
|
108 |
+
answer = gr.Textbox(
|
109 |
+
label="Answer:",
|
110 |
+
placeholder="Answer will appear here.",
|
111 |
+
lines=20,
|
112 |
+
)
|
113 |
+
with gr.Row():
|
114 |
+
with gr.Accordion("Expand for examples:", open=False):
|
115 |
+
gr.Examples(
|
116 |
+
examples=[
|
117 |
+
[
|
118 |
+
"assets/examples/1.png",
|
119 |
+
],
|
120 |
+
[
|
121 |
+
"assets/examples/2.png",
|
122 |
+
],
|
123 |
+
[
|
124 |
+
"assets/examples/3.png",
|
125 |
+
],
|
126 |
+
[
|
127 |
+
"assets/examples/4.png",
|
128 |
+
],
|
129 |
+
[
|
130 |
+
"assets/examples/5.png",
|
131 |
+
],
|
132 |
+
[
|
133 |
+
"assets/examples/6.png",
|
134 |
+
],
|
135 |
+
[
|
136 |
+
"assets/examples/7.png",
|
137 |
+
],
|
138 |
+
[
|
139 |
+
"assets/examples/8.png",
|
140 |
+
],
|
141 |
+
[
|
142 |
+
"assets/examples/9.png",
|
143 |
+
],
|
144 |
+
[
|
145 |
+
"assets/examples/10.png",
|
146 |
+
],
|
147 |
+
],
|
148 |
+
fn=solve,
|
149 |
+
inputs=[image],
|
150 |
+
outputs=[answer],
|
151 |
+
run_on_click=True,
|
152 |
+
)
|
153 |
+
submit_button.click(
|
154 |
+
fn=solve,
|
155 |
+
inputs=[image],
|
156 |
+
outputs=[answer],
|
157 |
+
)
|
158 |
+
clear_button.click(fn=clear, inputs=[], outputs=[image, answer])
|
159 |
+
image.clear(fn=clear, inputs=[], outputs=[image, answer])
|
160 |
+
|
161 |
+
return app
|
162 |
+
|
163 |
+
|
164 |
+
if __name__ == "__main__":
|
165 |
+
mongo.fetch_curr_access_count()
|
166 |
+
app = create_interface()
|
167 |
+
app.launch()
|
src/mongo_utils.py
ADDED
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pymongo
|
2 |
+
from app_config import config
|
3 |
+
|
4 |
+
|
5 |
+
# Generic functions
|
6 |
+
def get_db_client():
|
7 |
+
"""Returns MongoDB client object, connect to MongoDB Atlas instance if required"""
|
8 |
+
try:
|
9 |
+
if config.mongo_client == None:
|
10 |
+
client = pymongo.MongoClient(config.MONGO_CONN_STR)
|
11 |
+
config.mongo_client = client
|
12 |
+
except Exception as e:
|
13 |
+
print(e)
|
14 |
+
return config.mongo_client
|
15 |
+
|
16 |
+
|
17 |
+
def fetch_document(client, db, collection):
|
18 |
+
"""Get a single document from the provided db and collection"""
|
19 |
+
try:
|
20 |
+
document = client[db][collection].find_one()
|
21 |
+
except Exception as e:
|
22 |
+
print(e)
|
23 |
+
return document
|
24 |
+
|
25 |
+
|
26 |
+
def update_document(client, db, collection, key, value):
|
27 |
+
"""Update the passed key in the document for provided db and collection"""
|
28 |
+
try:
|
29 |
+
document = fetch_document(client, db, collection)
|
30 |
+
client[db][collection].update_one(
|
31 |
+
{"_id": document["_id"]},
|
32 |
+
{"$set": {key: value}},
|
33 |
+
)
|
34 |
+
except Exception as e:
|
35 |
+
print(e)
|
36 |
+
|
37 |
+
|
38 |
+
# Use case specific functions
|
39 |
+
def fetch_curr_access_count():
|
40 |
+
client = get_db_client()
|
41 |
+
curr_count = fetch_document(
|
42 |
+
client=client, db=config.db, collection=config.collection
|
43 |
+
)[config.key]
|
44 |
+
config.openai_curr_access_count = curr_count
|
45 |
+
|
46 |
+
|
47 |
+
def increment_curr_access_count():
|
48 |
+
client = get_db_client()
|
49 |
+
updated_count = config.openai_curr_access_count + 1
|
50 |
+
update_document(
|
51 |
+
client=client,
|
52 |
+
db=config.db,
|
53 |
+
collection=config.collection,
|
54 |
+
key=config.key,
|
55 |
+
value=updated_count,
|
56 |
+
)
|
57 |
+
config.openai_curr_access_count = updated_count
|
src/server.py
CHANGED
@@ -1,43 +1,37 @@
|
|
1 |
-
import base64
|
2 |
import requests
|
3 |
from app_config import config
|
|
|
|
|
4 |
|
|
|
5 |
|
6 |
-
# Function to encode the local image into base64 to be send over HTTP
|
7 |
-
def local_image_to_url(image_path):
|
8 |
-
with open(image_path, "rb") as image_file:
|
9 |
-
base64_image = base64.b64encode(image_file.read()).decode("utf-8")
|
10 |
-
return {"url": f"data:image/jpeg;base64,{base64_image}"}
|
11 |
|
|
|
|
|
12 |
|
13 |
-
|
14 |
-
|
15 |
-
|
|
|
|
|
|
|
|
|
16 |
headers = {
|
17 |
"Content-Type": "application/json",
|
18 |
-
"Authorization": f"Bearer {config.
|
19 |
}
|
20 |
payload = {
|
21 |
"model": "gpt-4-vision-preview",
|
22 |
-
"temperature": 0.2,
|
23 |
"messages": [
|
24 |
-
{
|
25 |
-
"role": "system",
|
26 |
-
"contents": """You an expert primary school maths teacher. Given an image
|
27 |
-
of a primary school maths problem you can analyze the problem and produce
|
28 |
-
a detailed solution with explanation."""
|
29 |
-
# In addition to this, given a
|
30 |
-
# collection images from primary school maths text book you can generate
|
31 |
-
# questions and there answers based on the topic shown in text book images,
|
32 |
-
# you will provide the detailed answers with explanation""",
|
33 |
-
},
|
34 |
{
|
35 |
"role": "user",
|
36 |
"content": [
|
37 |
-
{"type": "text", "text":
|
38 |
{
|
39 |
"type": "image_url",
|
40 |
-
"image_url":
|
41 |
},
|
42 |
],
|
43 |
},
|
@@ -45,16 +39,15 @@ def analyze_single_image(image_path: str, instruction: str, mode="url"): # "loc
|
|
45 |
"max_tokens": 600,
|
46 |
}
|
47 |
|
48 |
-
response = requests.post(config.
|
49 |
return response.json()
|
50 |
|
51 |
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
print(output["choices"][0]["message"]["content"])
|
60 |
return output["choices"][0]["message"]["content"]
|
|
|
|
|
1 |
import requests
|
2 |
from app_config import config
|
3 |
+
from fastapi import FastAPI
|
4 |
+
from pydantic import BaseModel
|
5 |
|
6 |
+
guruzee = FastAPI()
|
7 |
|
|
|
|
|
|
|
|
|
|
|
8 |
|
9 |
+
class SingleImageData(BaseModel):
|
10 |
+
data: str
|
11 |
|
12 |
+
|
13 |
+
def __analyze_single_image(image_data: str):
|
14 |
+
"""
|
15 |
+
Sends the user supplied image with system instructions to GPT-4, returns the
|
16 |
+
received response in JSON format.
|
17 |
+
"""
|
18 |
+
image_data = {"url": f"data:image/jpeg;base64,{image_data}"}
|
19 |
headers = {
|
20 |
"Content-Type": "application/json",
|
21 |
+
"Authorization": f"Bearer {config.OPENAI_KEY}",
|
22 |
}
|
23 |
payload = {
|
24 |
"model": "gpt-4-vision-preview",
|
25 |
+
"temperature": 0.2, # low temperature because we want deterministic responses
|
26 |
"messages": [
|
27 |
+
{"role": "system", "content": config.solver_persona},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
{
|
29 |
"role": "user",
|
30 |
"content": [
|
31 |
+
{"type": "text", "text": config.solver_instruction},
|
32 |
{
|
33 |
"type": "image_url",
|
34 |
+
"image_url": image_data,
|
35 |
},
|
36 |
],
|
37 |
},
|
|
|
39 |
"max_tokens": 600,
|
40 |
}
|
41 |
|
42 |
+
response = requests.post(config.OPENAI_API_ENDPOINT, headers=headers, json=payload)
|
43 |
return response.json()
|
44 |
|
45 |
|
46 |
+
@guruzee.post("/solve")
|
47 |
+
async def solve(image: SingleImageData):
|
48 |
+
"""
|
49 |
+
Invokes the OpenAI API passing the raw image bytes and returns the
|
50 |
+
response to client
|
51 |
+
"""
|
52 |
+
output = __analyze_single_image(image.data)
|
|
|
53 |
return output["choices"][0]["message"]["content"]
|
src/style.css
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.orange-button {
|
2 |
+
background-color: orange !important;
|
3 |
+
}
|
4 |
+
|
5 |
+
.gray-button {
|
6 |
+
background-color: #5e6061 !important;
|
7 |
+
}
|
8 |
+
|
9 |
+
footer {
|
10 |
+
visibility: hidden;
|
11 |
+
}
|