uploadfiles
Browse files- Dockerfile +20 -0
- app.py +59 -0
- requirements.txt +9 -0
- src/helper.py +10 -0
- src/prompt.py +9 -0
- static/style.css +167 -0
- templates/index.html +115 -0
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use official Python image
|
| 2 |
+
FROM python:3.10
|
| 3 |
+
|
| 4 |
+
# Set working directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Copy requirements
|
| 8 |
+
COPY requirements.txt /app/
|
| 9 |
+
|
| 10 |
+
# Install Python dependencies
|
| 11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 12 |
+
|
| 13 |
+
# Copy all application files
|
| 14 |
+
COPY . /app/
|
| 15 |
+
|
| 16 |
+
# Expose required port for Hugging Face
|
| 17 |
+
EXPOSE 7860
|
| 18 |
+
|
| 19 |
+
# Command to run Flask app
|
| 20 |
+
CMD ["python", "app.py"]
|
app.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain_google_genai import ChatGoogleGenerativeAI
|
| 2 |
+
from langchain_core.prompts import ChatPromptTemplate
|
| 3 |
+
from langchain.chains.combine_documents import create_stuff_documents_chain
|
| 4 |
+
from langchain.chains import create_retrieval_chain
|
| 5 |
+
from src.helper import download_embeding
|
| 6 |
+
from langchain_pinecone import PineconeVectorStore
|
| 7 |
+
from src.prompt import system_prompt
|
| 8 |
+
import os
|
| 9 |
+
from flask import Flask, render_template, jsonify, request
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
from pinecone import Pinecone
|
| 12 |
+
load_dotenv()
|
| 13 |
+
|
| 14 |
+
model = ChatGoogleGenerativeAI(model="gemini-2.5-flash", google_api_key=os.getenv("GOOGLE_API_KEY"))
|
| 15 |
+
app=Flask(__name__)
|
| 16 |
+
PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
|
| 17 |
+
pc = Pinecone(api_key=PINECONE_API_KEY)
|
| 18 |
+
index_name='medicalchatbot'
|
| 19 |
+
embedding=download_embeding()
|
| 20 |
+
|
| 21 |
+
docsearch=PineconeVectorStore.from_existing_index(
|
| 22 |
+
index_name=index_name,
|
| 23 |
+
embedding=embedding
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
retiver=docsearch.as_retriever(search_type="similarity", search_kwargs={"k": 3})
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
prompt = ChatPromptTemplate.from_messages(
|
| 30 |
+
[
|
| 31 |
+
("system", system_prompt),
|
| 32 |
+
("human", "{input}"),
|
| 33 |
+
]
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
question_answer_chain=create_stuff_documents_chain(model,prompt)
|
| 37 |
+
rag_chain=create_retrieval_chain(retiver,question_answer_chain)
|
| 38 |
+
|
| 39 |
+
@app.route("/")
|
| 40 |
+
def index():
|
| 41 |
+
return render_template('index.html')
|
| 42 |
+
|
| 43 |
+
@app.route("/get", methods=["GET", "POST"])
|
| 44 |
+
def chat():
|
| 45 |
+
msg = request.form.get("msg", "").strip()
|
| 46 |
+
if not msg:
|
| 47 |
+
return "Please enter a question.", 400
|
| 48 |
+
|
| 49 |
+
response = rag_chain.invoke({"input": msg})
|
| 50 |
+
answer = response.get("answer", "Sorry, I couldn't generate a response.")
|
| 51 |
+
print("Response:", answer)
|
| 52 |
+
return answer
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
if __name__ == '__main__':
|
| 58 |
+
port = int(os.environ.get("PORT", 7860))
|
| 59 |
+
app.run(host="0.0.0.0", port=port)
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
langchain==0.3.26
|
| 2 |
+
langchain-community==0.3.26
|
| 3 |
+
langchain-core==0.3.26
|
| 4 |
+
flask==3.1.1
|
| 5 |
+
sentence-transformers==5.1.2
|
| 6 |
+
python-dotenv==1.0.1
|
| 7 |
+
langchain-pinecone==0.2.2
|
| 8 |
+
langchain-google-genai==2.0.4
|
| 9 |
+
pinecone-client==4.1.2
|
src/helper.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from langchain.embeddings import HuggingFaceEmbeddings
|
| 2 |
+
import torch
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def download_embeding():
|
| 6 |
+
embedding = HuggingFaceEmbeddings(
|
| 7 |
+
model_name="sentence-transformers/all-MiniLM-L6-v2",
|
| 8 |
+
model_kwargs={"device": "cuda" if torch.cuda.is_available() else "cpu"}
|
| 9 |
+
)
|
| 10 |
+
return embedding
|
src/prompt.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
system_prompt = (
|
| 2 |
+
"You are an Medical assistant for question-answering tasks. "
|
| 3 |
+
"Use the following pieces of retrieved context to answer "
|
| 4 |
+
"the question. If you don't know the answer, say that you "
|
| 5 |
+
"don't know. Use three sentences maximum and keep the "
|
| 6 |
+
"answer concise."
|
| 7 |
+
"\n\n"
|
| 8 |
+
"{context}"
|
| 9 |
+
)
|
static/style.css
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body.dark {
|
| 2 |
+
background: #1c1c1c !important;
|
| 3 |
+
color: white !important;
|
| 4 |
+
}
|
| 5 |
+
|
| 6 |
+
body.dark .chat-container {
|
| 7 |
+
background: #2b2b2b;
|
| 8 |
+
color: white;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
body.dark .chat-box {
|
| 12 |
+
background: #333;
|
| 13 |
+
border-color: #444;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
body.dark .message.user {
|
| 17 |
+
background: #4c6ef5;
|
| 18 |
+
color: white;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
body.dark .message.bot {
|
| 22 |
+
background: #555;
|
| 23 |
+
color: white;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
body.dark #sendBtn {
|
| 27 |
+
background: #4c6ef5;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
body.dark #themeToggle {
|
| 31 |
+
color: white;
|
| 32 |
+
border-color: white;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/* Loading Animation */
|
| 36 |
+
.loading {
|
| 37 |
+
display: flex;
|
| 38 |
+
justify-content: center;
|
| 39 |
+
margin: 10px 0;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.spinner {
|
| 43 |
+
width: 30px;
|
| 44 |
+
height: 30px;
|
| 45 |
+
border: 4px solid #ccc;
|
| 46 |
+
border-top: 4px solid #007bff;
|
| 47 |
+
border-radius: 50%;
|
| 48 |
+
animation: spin 1s linear infinite;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
@keyframes spin {
|
| 52 |
+
0% { transform: rotate(0deg); }
|
| 53 |
+
100% { transform: rotate(360deg); }
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
/* Doctor Avatar + Bottom Bar */
|
| 57 |
+
.bottom-bar {
|
| 58 |
+
display: flex;
|
| 59 |
+
justify-content: space-between;
|
| 60 |
+
align-items: center;
|
| 61 |
+
margin-top: 10px;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.doctor-avatar {
|
| 65 |
+
width: 40px;
|
| 66 |
+
height: 40px;
|
| 67 |
+
border-radius: 50%;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
body {
|
| 71 |
+
background: linear-gradient(135deg, #5a8dee, #6ed3cf);
|
| 72 |
+
font-family: 'Poppins', sans-serif;
|
| 73 |
+
margin: 0;
|
| 74 |
+
padding: 20px;
|
| 75 |
+
display: flex;
|
| 76 |
+
justify-content: center;
|
| 77 |
+
align-items: center;
|
| 78 |
+
height: 100vh;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
.chat-container {
|
| 82 |
+
background: #ffffff;
|
| 83 |
+
width: 420px;
|
| 84 |
+
border-radius: 20px;
|
| 85 |
+
padding: 20px;
|
| 86 |
+
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
|
| 87 |
+
animation: fadeIn 0.7s ease-in-out;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.header {
|
| 91 |
+
text-align: center;
|
| 92 |
+
margin-bottom: 20px;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.header .logo {
|
| 96 |
+
width: 70px;
|
| 97 |
+
margin-bottom: 10px;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.header h2 {
|
| 101 |
+
margin: 0;
|
| 102 |
+
font-weight: 600;
|
| 103 |
+
color: #333;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.header p {
|
| 107 |
+
font-size: 14px;
|
| 108 |
+
color: #666;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
.chat-box {
|
| 112 |
+
height: 360px;
|
| 113 |
+
overflow-y: auto;
|
| 114 |
+
border-radius: 10px;
|
| 115 |
+
padding: 10px;
|
| 116 |
+
background: #f7f9fc;
|
| 117 |
+
margin-bottom: 15px;
|
| 118 |
+
border: 1px solid #e5e5e5;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
.message {
|
| 122 |
+
padding: 10px;
|
| 123 |
+
margin-bottom: 10px;
|
| 124 |
+
border-radius: 10px;
|
| 125 |
+
max-width: 85%;
|
| 126 |
+
animation: slideIn 0.4s ease-in-out;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.message.user {
|
| 130 |
+
background: #d1e8ff;
|
| 131 |
+
align-self: flex-end;
|
| 132 |
+
text-align: right;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.message.bot {
|
| 136 |
+
background: #e6e6e6;
|
| 137 |
+
text-align: left;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.input-area textarea {
|
| 141 |
+
border-radius: 10px;
|
| 142 |
+
resize: none;
|
| 143 |
+
border: 1px solid #bcd;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
#sendBtn {
|
| 147 |
+
border-radius: 10px;
|
| 148 |
+
font-weight: 600;
|
| 149 |
+
padding: 10px;
|
| 150 |
+
background: #0066ff;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
#sendBtn:hover {
|
| 154 |
+
background: #004ecc;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
/* Animations */
|
| 158 |
+
@keyframes fadeIn {
|
| 159 |
+
from { opacity: 0; transform: translateY(20px); }
|
| 160 |
+
to { opacity: 1; transform: translateY(0); }
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
@keyframes slideIn {
|
| 164 |
+
from { opacity: 0; transform: translateX(20px); }
|
| 165 |
+
to { opacity: 1; transform: translateX(0); }
|
| 166 |
+
}
|
| 167 |
+
to { opacity: 1; transform: translateX(0); }
|
templates/index.html
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!-- index.html -->
|
| 2 |
+
<!DOCTYPE html>
|
| 3 |
+
<html lang="en">
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>Medical Assistant Chatbot</title>
|
| 8 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}" />
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
+
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600&display=swap" rel="stylesheet">
|
| 12 |
+
<!-- Bootstrap -->
|
| 13 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 14 |
+
</head>
|
| 15 |
+
<body>
|
| 16 |
+
|
| 17 |
+
<div class="chat-container">
|
| 18 |
+
<div class="header">
|
| 19 |
+
<img src="https://cdn-icons-png.flaticon.com/512/921/921347.png" alt="Medical Icon" class="logo" />
|
| 20 |
+
<h2>AI Medical Assistant</h2>
|
| 21 |
+
<p>Your smart healthcare companion</p>
|
| 22 |
+
</div>
|
| 23 |
+
|
| 24 |
+
<div class="chat-box" id="chatBox">
|
| 25 |
+
<!-- Chat messages will appear here -->
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<div class="input-area">
|
| 29 |
+
<textarea id="userInput" class="form-control" placeholder="Describe your symptoms or ask a medical question..." rows="3"></textarea>
|
| 30 |
+
<button id="sendBtn" class="btn btn-primary mt-2 w-100">Send</button>
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<!-- Doctor Avatar + Toggle Theme -->
|
| 34 |
+
<div class="bottom-bar">
|
| 35 |
+
<img src="https://cdn-icons-png.flaticon.com/512/387/387561.png" class="doctor-avatar" />
|
| 36 |
+
<button id="themeToggle" class="btn btn-outline-secondary btn-sm">π Dark Mode</button>
|
| 37 |
+
</div>
|
| 38 |
+
|
| 39 |
+
</div>
|
| 40 |
+
|
| 41 |
+
<script>
|
| 42 |
+
const chatBox = document.getElementById('chatBox');
|
| 43 |
+
const sendBtn = document.getElementById('sendBtn');
|
| 44 |
+
const userInput = document.getElementById('userInput');
|
| 45 |
+
|
| 46 |
+
function appendMessage(sender, message) {
|
| 47 |
+
const msg = document.createElement('div');
|
| 48 |
+
msg.classList.add('message', sender);
|
| 49 |
+
msg.innerHTML = `<p>${message}</p>`;
|
| 50 |
+
chatBox.appendChild(msg);
|
| 51 |
+
chatBox.scrollTop = chatBox.scrollHeight;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
function showLoading() {
|
| 55 |
+
const loading = document.createElement('div');
|
| 56 |
+
loading.classList.add('loading');
|
| 57 |
+
loading.innerHTML = `<div class="spinner"></div>`;
|
| 58 |
+
chatBox.appendChild(loading);
|
| 59 |
+
chatBox.scrollTop = chatBox.scrollHeight;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
function hideLoading() {
|
| 63 |
+
const loading = document.querySelector('.loading');
|
| 64 |
+
if (loading) loading.remove();
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
sendBtn.addEventListener('click', sendMessage);
|
| 68 |
+
userInput.addEventListener('keypress', (e) => {
|
| 69 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 70 |
+
e.preventDefault();
|
| 71 |
+
sendMessage();
|
| 72 |
+
}
|
| 73 |
+
});
|
| 74 |
+
|
| 75 |
+
function sendMessage() {
|
| 76 |
+
const text = userInput.value.trim();
|
| 77 |
+
if (!text) return;
|
| 78 |
+
|
| 79 |
+
appendMessage('user', text);
|
| 80 |
+
userInput.value = '';
|
| 81 |
+
|
| 82 |
+
showLoading();
|
| 83 |
+
|
| 84 |
+
fetch("/get", {
|
| 85 |
+
method: "POST",
|
| 86 |
+
headers: {
|
| 87 |
+
"Content-Type": "application/x-www-form-urlencoded",
|
| 88 |
+
},
|
| 89 |
+
body: `msg=${encodeURIComponent(text)}`
|
| 90 |
+
})
|
| 91 |
+
.then(res => res.text())
|
| 92 |
+
.then(data => {
|
| 93 |
+
hideLoading();
|
| 94 |
+
appendMessage("bot", data);
|
| 95 |
+
})
|
| 96 |
+
.catch(err => {
|
| 97 |
+
hideLoading();
|
| 98 |
+
appendMessage("bot", "Error connecting to server.");
|
| 99 |
+
console.error(err);
|
| 100 |
+
});
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// Dark / Light Theme Toggle
|
| 104 |
+
const themeToggle = document.getElementById('themeToggle');
|
| 105 |
+
themeToggle.addEventListener('click', () => {
|
| 106 |
+
document.body.classList.toggle('dark');
|
| 107 |
+
themeToggle.textContent = document.body.classList.contains('dark')
|
| 108 |
+
? "β Light Mode"
|
| 109 |
+
: "π Dark Mode";
|
| 110 |
+
});
|
| 111 |
+
</script>
|
| 112 |
+
|
| 113 |
+
</body>
|
| 114 |
+
</html>
|
| 115 |
+
|