add a AI chatbot, rename course names

This commit is contained in:
BuffTechTalk
2025-01-10 14:29:06 -06:00
parent 9a28b3eaba
commit ab0e64777e
30 changed files with 430 additions and 76 deletions

20
app.py
View File

@@ -15,6 +15,8 @@ page_label = nv()
if page_label == "Homepage":
pg.home()
elif page_label == "BuffBot":
pg.buffbot()
elif page_label == "Outstanding Members":
pg.outstanding_members()
@@ -33,14 +35,16 @@ elif page_label == "Join Us":
# block of PythonX lessons
elif page_label == "About PythonX":
pg.pythonx_homepage()
elif page_label == "Lesson1":
pg.pythonx_lesson1()
elif page_label == "Lesson2":
pg.pythonx_lesson2()
elif page_label == "Lesson3":
pg.pythonx_lesson3()
elif page_label == "Lesson4":
pg.pythonx_lesson4()
elif page_label == "Introduction":
pg.pythonx_introduction()
elif page_label == "WordCloud":
pg.pythonx_wordcloud()
elif page_label == "Finance":
pg.pythonx_finance()
elif page_label == "GeoMap":
pg.pythonx_geomap()
elif page_label == "BuffBot":
pg.pythonx_buffbot()
# block of CIS Tech Challenge Event
elif page_label == "CIS Tech Challenge":

16
app_config.json Normal file
View File

@@ -0,0 +1,16 @@
{
"deepseek":{
"api_url": "https://api.deepseek.com",
"api_key": "sk-12165b127043441697a8940918e207ac"
},
"ollama":{
"api_url": "http://localhost:11434/v1",
"api_key": "ollama"
},
"send_email": {
"sender_email": "noreply@buffteks.org",
"password": "cidm4360fall2024@*"
}
}

View File

@@ -1,6 +0,0 @@
{
"api_url": "https://api.deepseek.com",
"api_key": "sk-12165b127043441697a8940918e207ac",
"ollama_api_url": "http://localhost:11434/v1",
"ollama_api_key": "ollama"
}

View File

@@ -12,7 +12,7 @@ with st.expander("See Source Code"):
st.code(f.read(), language="python")
# Load API credentials from config.json
with open('chatbot_config.json') as config_file:
with open('app_config.json') as config_file:
config = json.load(config_file)
openai_api_base_url = config["api_url"]
openai_api_key = config["api_key"]

View File

@@ -15,14 +15,14 @@ st.markdown("<h1 style='text-align: center; color: #451002;'>BuffBot🦬</h1>",
# st.subheader()
st.info("Powered by llama3.2:1b model via [Ollama](https://ollama.com/library/llama3.2:1b)!")
with st.expander("See Source Code"):
with open(__file__, "r") as f:
st.code(f.read(), language="python")
with open(__file__, "r") as f:
st.code(f.read(), language="python")
# Load API credentials from config.json
with open('chatbot_config.json') as config_file:
with open('app_config.json') as config_file:
config = json.load(config_file)
api_base_url = config["ollama_api_url"]
api_key = config["ollama_api_key"]
api_base_url = config["ollama"]["api_url"]
api_key = config["ollama"]["api_key"]
client = OpenAI(api_key=api_key, base_url=api_base_url)

3
new_members.json Normal file
View File

@@ -0,0 +1,3 @@
[
]

View File

@@ -106,7 +106,9 @@ def send_email(sender_email, password, to_email, subject):
server.starttls() # Enable TLS encryption for secure connection
server.login(sender_email, password) # Log in with the sender's email and password
server.send_message(message) # Send the email message
print("Email sent successfully!")
except Exception as e:
# Catch and display any exceptions that occur during the sending process
print(f"Failed to send email: {str(e)}")

View File

@@ -6,10 +6,11 @@ from .join_us import join_us
from .testing import testing
from .reference import reference
from .pythonx_lessons_pages.pythonx_homepage import pythonx_homepage
from .pythonx_lessons_pages.pythonx_lesson1 import pythonx_lesson1
from .pythonx_lessons_pages.pythonx_lesson2 import pythonx_lesson2
from .pythonx_lessons_pages.pythonx_lesson3 import pythonx_lesson3
from .pythonx_lessons_pages.pythonx_lesson4 import pythonx_lesson4
from .pythonx_lessons_pages.pythonx_introduction import pythonx_introduction
from .buff_bot import buffbot
from .pythonx_lessons_pages.pythonx_finance import pythonx_finance
from .pythonx_lessons_pages.pythonx_geomap import pythonx_geomap
from .pythonx_lessons_pages.pythonx_wordcloud import pythonx_wordcloud
from .outstanding_members import outstanding_members
from .cis_tech_challenge_pages.cis_tech_challenge_homepage import cis_tech_challenge_homepage
from .coreteks_pages.coreteks_homepage import coreteks_homepage

Binary file not shown.

119
webpages/buff_bot.py Normal file
View File

@@ -0,0 +1,119 @@
import streamlit as st
from openai import OpenAI # OpenAI compatibility
import json
# reference:
# - Use OpenAI to connect Ollama: https://ollama.com/blog/openai-compatibility
# - Build Chatbot with streamlit: https://streamlit.io/generative-ai
# - Ollama docker: https://hub.docker.com/r/ollama/ollama
# - [TBD] Finetune: https://docs.loopin.network/tutorials/LLM/llama3-finetune
# Clear chat history
def clear_chat():
st.session_state.messages = []
st.toast("Chat Cleaned", icon="🧹")
def buffbot():
# Set up the Streamlit app
st.markdown("<h1 style='text-align: center; color: #451002;'>BuffBot🦬</h1>", unsafe_allow_html=True)
st.markdown("<h5 style='text-align: center;'> Your friendly AI chatbot powered by LLM! 🤖 </h3>", unsafe_allow_html=True)
# Display info and source code
with st.expander("See Source Code"):
with open(__file__, "r", encoding="utf-8") as f:
st.code(f.read(), language="python")
# Select AI model for chatbot
model_options = ["llama3.2:1b", "deepseek-chat", ]
# on_change callback to clear chat history when model is changed
selected_model = st.selectbox("**👉 Please select a model to start**", model_options, on_change=clear_chat)
# Initialize session state to store chat history and message count
if "messages" not in st.session_state:
st.session_state.messages = []
# Initialize message count
if "message_count" not in st.session_state:
st.session_state.message_count = 0
# Load API credentials from config.json
# the config file contains the API key and base URL for the selected model
"""
{
"deepseek":{
"api_url": "https://api.deepseek.com",
"api_key": "YOUR_API_KEY"
},
"ollama":{
"api_url": "http://localhost:11434/v1",
"api_key": "ollama"
}
}
"""
# The API key and base URL are loaded based on the selected model
with open('app_config.json') as config_file:
config = json.load(config_file)
# deepseek-chat model, online API
if selected_model == "deepseek-chat":
api_base_url = config["deepseek"]["api_url"]
api_key = config["deepseek"]["api_key"]
st.info("Powered by the online [DeepSeek](https://www.deepseek.com/) API!\
Just a heads up, you have 10 messages to use.")
# Set the maximum number of user messages
MAX_USER_MESSAGES = 10
# llama3.2:1b model, local API
if selected_model == "llama3.2:1b":
api_base_url = config["ollama"]["api_url"]
api_key = config["ollama"]["api_key"]
st.info("Powered by local llama3.2:1b model via [Ollama](https://ollama.com/library/llama3.2:1b)!\
Just a heads up, you have 100 messages to use.")
MAX_USER_MESSAGES = 100
# Initialize OpenAI client to connect with the selected model API
client = OpenAI(api_key=api_key, base_url=api_base_url)
# print welcome message
with st.chat_message("assistant", avatar="🦬"):
st.markdown("Welcome to BuffBot! What Can I Do for You Today?🌞")
# Display chat history with different avatars for user and AI assistant
for message in st.session_state.messages:
if message["role"] == "user":
avatar="🤠"
else:
avatar="🦬"
with st.chat_message(message["role"], avatar=avatar):
st.markdown(message["content"])
# Get user input
if prompt := st.chat_input("Type your message here..."):
# Add user message to chat history
st.session_state.messages.append({"role": "user", "content": prompt})
# Display user message with cowboy avatar
with st.chat_message("user", avatar="🤠"):
st.markdown(prompt)
# Generate reply
with st.chat_message("assistant", avatar="🦬"):
with st.spinner('Thinking...'):
# Call the selected model API to generate a response
stream = client.chat.completions.create(
model=selected_model,
messages=[
{"role": m["role"], "content": m["content"]}
for m in st.session_state.messages
],
stream=True, # stream the response
)
# Display the response from the model API
response = st.write_stream(stream)
# Add the AI assistant response to the chat history
st.session_state.messages.append({"role": "assistant", "content": response})
# Clear chat history
if st.button("Clear Chat"):
clear_chat()
st.rerun()

View File

@@ -27,7 +27,7 @@ def coreteks_homepage():
# for linux talk
st.subheader(" :two: Linux Security and Web Server Setup")
st.image("./images/11-19-2024-CaddyAndLinux.jpg", width= 500)
st.image("./images/11-19-2024-CaddyAndLinux.jpg", width= 700)
st.link_button(label="Watch Video Recording", use_container_width=True, type="primary", url="https://wtamu0-my.sharepoint.com/:v:/g/personal/czhang_wtamu_edu/EdgZHrnkqihNrOZGYAT6O2MBSRBOBMv3czD_uIE21KgsWw?nav=eyJyZWZlcnJhbEluZm8iOnsicmVmZXJyYWxBcHAiOiJPbmVEcml2ZUZvckJ1c2luZXNzIiwicmVmZXJyYWxBcHBQbGF0Zm9ybSI6IldlYiIsInJlZmVycmFsTW9kZSI6InZpZXciLCJyZWZlcnJhbFZpZXciOiJNeUZpbGVzTGlua0NvcHkifX0&e=obIoyY", )
st.link_button(label="Download PDF Instruction", use_container_width=True, type="primary", url="https://wtamu0-my.sharepoint.com/:b:/g/personal/czhang_wtamu_edu/Eaygqoc_FqxNgYj3BlEOXhsBzOj-BWTbNKwkpJEYSDBwgA?e=K7qxSU", )
st.divider()

View File

@@ -1,4 +1,136 @@
import streamlit as st
from .join_us import join_us
def home():
st.html("./webpages/buffteks.html")
# Custom CSS to style the page
st.markdown("""
<style>
body {
background-color: #f4f4f4;
}
.header-content {
background-color: #ffffff;
color: #450012;
max-width: 800px;
margin: 0 auto;
padding: 1rem;
text-align: center;
}
.container {
max-width: 800px;
margin: 0 auto 2rem;
padding: 1rem;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h2 {
border-bottom: 2px solid #333;
padding-bottom: 10px;
}
ul {
list-style-type: none;
padding: 0;
}
ul li {
padding: 10px 0;
}
.faculty-row {
display: flex;
justify-content: space-between;
margin-top: 10px;
}
.faculty {
text-align: center;
flex: 1;
margin: 0 15px;
}
.faculty img {
height: 210px;
width: 140px;
margin-bottom: 10px;
}
.faculty-info {
margin-top: 10px;
}
.join {
text-align: center;
margin-top: 2rem;
}
</style>
""", unsafe_allow_html=True)
# Header
st.markdown("""
<div class="header-content">
<h1>BuffTeks Student Organization</h1>
<p>Building Skills, Crafting Code, Bridging Communities</p>
</div>
""", unsafe_allow_html=True)
# Our Mission
st.markdown("""
<section>
<h2>Our Mission</h2>
<p>Empower members with advanced software development knowledge, foster new skills and technologies in a collaborative and engaging community.</p>
</section>
""", unsafe_allow_html=True)
# Our Activities
st.markdown("""
<section>
<h2>Our Activities</h2>
<ul>
<li><strong>BuffTeks Project:</strong> Faculty-led coding projects to provide IT solutions and support to problems facing local communities as part of an experiential learning effort.</li>
<li><strong>BuffTeks Classroom:</strong> An open learning platform devoted to sharing knowledge of information technology including Python programming and web application development.</li>
<li><strong>BuffTeks Event:</strong> Host competitions and hackathons that empower students to use classroom knowledge for real-world solutions.</li>
</ul>
</section>
""", unsafe_allow_html=True)
# Faculty Advisors
st.markdown("""
<section>
<h2>Faculty Advisors</h2>
<div class="faculty-container">
<div class="faculty-row">
<div class="faculty">
<img src="https://www.wtamu.edu/_files/images/academics/college-business/headshots/babb-jeffry-22.png" alt="Dr. Jeffry Babb">
<div class="faculty-info">
<strong>Dr. Jeffry Babb</strong><br>
BuffTeks Founder<br>
</div>
</div>
<div class="faculty">
<img src="https://www.wtamu.edu/_files/images/academics/college-business/headshots/zhang-carl-22.png" alt="Dr. Carl Zhang">
<div class="faculty-info">
<strong>Dr. Carl Zhang</strong><br>
<a href="mailto:czhang@wtamu.edu">czhang@wtamu.edu</a>
</div>
</div>
<div class="faculty">
<img src="https://www.wtamu.edu/_files/images/academics/college-business/headshots/dana-kareem-22.png" alt="Mr. Kareem Dana">
<div class="faculty-info">
<strong>Mr. Kareem Dana</strong><br>
<a href="mailto:kdana@wtamu.edu">kdana@wtamu.edu</a>
</div>
</div>
</div>
</div>
</section>
""", unsafe_allow_html=True)

View File

@@ -1,50 +1,103 @@
import streamlit as st
from .SendEmail import send_email
import time
import json
import re
# reset all inputs
def clear_text():
st.session_state["full_name"] = ""
st.session_state["email"] = ""
st.session_state["stu_major"] = None
st.session_state["other_major"] = ""
st.session_state["stu_year"] = None
st.session_state["skills_selection"] = []
st.session_state["other_skill"] = ""
def is_valid_email(email):
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email)
@st.dialog("Review Your Information")
def review(review_info):
st.write(review_info)
confirm_btn = st.button("Confirm and Send Email", on_click=clear_text)
if confirm_btn:
# save new member info to json file
try:
with open('new_members.json', 'r+') as file:
data = json.load(file)
data.append(review_info)
file.seek(0)
json.dump(data, file, indent=4)
except FileNotFoundError:
with open('new_members.json', 'w') as file:
json.dump([refiew_info], file, indent=4)
# load email config from app_config.json
with open('app_config.json') as config_file:
config = json.load(config_file)
email_config = config["send_email"]
# send email to new member
with st.spinner('📧 Sending Email...'):
send_email(
sender_email=email_config["sender_email"],
password=email_config["password"],
to_email=review_info["Email"],
subject=f"Welcome to join BuffTeks, {review_info["Full Name"]}!"
)
time.sleep(3)
st.success("🎉Thanks for submitting your information, you will receive an invitation email shortly. \
\nPlease contact Dr.Zhang (czhang@wtamu.edu) if you need any help.")
progress_text = "⏱️ Window will be closed in 10 seconds..."
my_bar = st.progress(0, text=progress_text)
for percent_complete in range(99):
time.sleep(0.1)
my_bar.progress(percent_complete + 1, text=progress_text)
time.sleep(1)
st.rerun()
def join_us():
st.title("Join Us")
st.write("Thank you for your interest in the BuffTeks student organization! Please share your information below, \
and our Faculty Advisor will send you an invitation to join our Discord channel. \
If you have any questions, feel free to contact our Faculty Advisor, Dr. Carl Zhang, at czhang@wtamu.edu.")
full_name = st.text_input("**Full Name**")
wt_email = st.text_input("**WT Email**")
full_name = st.text_input("**Full Name**", key="full_name")
email = st.text_input("**Email**", key="email", placeholder="WT email is preferred")
major_list = ["Accounting", "Computer Infromation Systems", "Economics", "Finance", "General Business", "Management", "Marketing", "Other"]
stu_major = st.radio("**Major/Field of Study**",major_list)
stu_major = st.radio("**Major/Field of Study**",major_list, key="stu_major")
if stu_major == "Other":
other_major = st.text_input("Please specify the other major")
other_major = st.text_input("Please specify the other major", key="other_major")
stu_major = other_major
year_of_study_list = ["Freshman", "Sophomore", "Junior", "Senior", "Graduate"]
stu_year = st.radio("**Year of Study**", year_of_study_list)
stu_year = st.radio("**Year of Study**", year_of_study_list, key="stu_year")
skills_options = ["Programming", "Cybersecurity", "Web Development", "Mobile App Development", "machine Learning/AI", "Data Analysis", "Other"]
skills_selection = st.multiselect(
label= "**Technical Skills You Are Interested In (Please check all that apply)**",
options = skills_options,
placeholder = "Choose all that apply")
placeholder = "Choose all that apply",
key="skills_selection")
if "Other" in skills_selection:
other_skill = st.text_input("**Please specify the other skills**")
other_skill = st.text_input("**Please specify the other skills**", key="other_skill")
skills_selection.remove("Other")
skills_selection.append(f"Other ({other_skill})")
skills_selection.append(f"{other_skill}")
next_btn = st.button("Next")
if next_btn:
if "wtamu.edu" not in wt_email:
st.warning("Please input your WT Email address to receive invitation")
with st.spinner('Sending Email...'):
send_email(sender_email="noreply@buffteks.org", password="cidm4360fall2024@*", to_email=wt_email, subject=f"Welcome to join ButtTeks, {full_name}!")
st.balloons()
st.success("Thanks for submitting your information, you will receive an invitation email shortly. \n Please contact Dr.Zhang (czhang@wtamu.edu) if you need any help.")
# @st.dialog("Check Email")
# def confirm_email(wt_email, full_name):
# st.write(f"Please double-check your **WT Email** in order to receive our invitation: \n {wt_email}")
# if st.button("Confirm & Submit"):
# send_email(sender_email="noreply@buffteks.org", password="cidm4360fall2024@*", to_email=wt_email, subject=f"Welcome to join ButtTeks, {full_name}!")
# st.rerun()
# return True
if not is_valid_email(email):
st.error("Please enter a valid email address.")
return
st.session_state.review_info = {
"Full Name": full_name,
"Email": email,
"Major/Field of Study": stu_major,
"Year of Study": stu_year,
"Technical Skills": skills_selection
}
review(review_info=st.session_state.review_info)

View File

@@ -13,23 +13,25 @@ def navigation_bar():
page_label = sac.menu([
sac.MenuItem('Homepage', icon='house'),
sac.MenuItem('BuffBot', icon='robot'),
sac.MenuItem('Outstanding Members', icon='award'),
sac.MenuItem("Join Us", icon='person-add'),
sac.MenuItem('BuffTeks Project', icon='bi bi-laptop'),
sac.MenuItem('BuffTeks Event', icon='calendar-event', children=[
sac.MenuItem('CIS Tech Challenge', icon='bi bi-trophy'),
]),
sac.MenuItem('BuffTeks Classroom', icon='book', children=[
sac.MenuItem('About Classroom', icon='question-circle'),
sac.MenuItem('PythonX', icon='bi bi-filetype-py', children=[
sac.MenuItem('About PythonX', icon='question-circle'),
sac.MenuItem('Lesson1', icon='1-square'),
sac.MenuItem('Lesson2', icon='2-square'),
sac.MenuItem('Lesson3', icon='3-square'),
sac.MenuItem('Lesson4', icon='4-square'),
sac.MenuItem('Introduction', icon='1-square'),
sac.MenuItem('WordCloud', icon='2-square'),
sac.MenuItem('Finance', icon='3-square'),
sac.MenuItem('GeoMap', icon='4-square'),
# sac.MenuItem('BuffBot', icon='5-square'),
]),
sac.MenuItem('CoreTeks', icon='bi bi-tools'),
]),
sac.MenuItem("Join Us", icon='person-add'),
sac.MenuItem('BuffTeks Event', icon='calendar-event', children=[
sac.MenuItem('CIS Tech Challenge', icon='bi bi-trophy'),
]),
# sac.MenuItem("Testing", icon='fingerprint'),
# sac.MenuItem(type='divider'),
# sac.MenuItem('Link', type='group', children=[

View File

@@ -4,7 +4,7 @@ import yfinance as yf
import plotly.express as px
from webpages import code_editor as ce
def pythonx_lesson3():
def pythonx_finance():
st.title("Lesson 3: Collecting Stock Data Through API")
st.header(":one: What is API")

View File

@@ -1,17 +1,17 @@
import streamlit as st
import pandas as pd
import folium
from streamlit_folium import folium_static
from geopy.geocoders import Nominatim
import sqlite3
from datetime import datetime
from folium.features import CustomIcon
import pandas as pd # package for database connection
import folium # package for creating maps
from streamlit_folium import folium_static # package for displaying maps on streamlit
from geopy.geocoders import Nominatim # package for geolocation conversion
import sqlite3 # package for database connection
from datetime import datetime # package for timestamp
from folium.features import CustomIcon # package for custom icons on map
def pythonx_lesson4():
def pythonx_geomap():
# Initialize the database
conn = sqlite3.connect('./files/student_locations.db')
# Create a table to store student locations
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS students
(id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -24,16 +24,40 @@ def pythonx_lesson4():
conn.commit()
st.title('Where are Members From')
st.title("Lesson 4: Geographical Data Visualization")
st.markdown("""
This lesson demonstrates how to handle geolocation data and visualize it using maps in Python.
The main functionalities include:
- **User Input for Location**: Users can input their city and state or city and country through a form.
- **Geolocation Processing**: The input location is processed to obtain latitude and longitude coordinates.
- **Data Storage**: The processed location data is saved to a [SQLite](https://www.geeksforgeeks.org/python-sqlite/) database.
- **Map Visualization**: All stored student locations are displayed on an interactive map.
- **Statistics Display**: The code also provides statistics on the total number of students, unique cities, and unique countries, along with the top 5 cities.
""")
# Display the source code
with st.expander("See Source Code"):
with open(__file__, "r", encoding="utf-8") as f:
st.code(f.read(), language="python")
st.subheader("Student Location Tracker")
# Input form for student location
with st.form("student_form"):
input_city = st.text_input("Enter your City, State (e.g.: Amarillo,TX), or City, Country (Toronto, Canada):")
submitted = st.form_submit_button("Submit")
if submitted:
if submitted:
# converting addresses (like "Mountain View, CA") into geographic
# coordinates (like latitude 37.423021 and longitude -122.083739)
lat, lon, city, state, country = get_location(input_city)
# Save the location data to the database
if lat and lon:
save_student(conn, city, state, country, lat, lon)
st.success(f"Location saved: {city}, {country}")
@@ -66,7 +90,7 @@ def pythonx_lesson4():
conn.close()
# convert address to latitude and longitude
def get_location(city):
geolocator = Nominatim(user_agent="student_location_app")
try:
@@ -82,7 +106,7 @@ def get_location(city):
st.write(e)
return None, None, None, None, None
# save student location to database
def save_student(conn, city, state, country, lat, lon):
c = conn.cursor()
timestamp = datetime.now()
@@ -90,11 +114,14 @@ def save_student(conn, city, state, country, lat, lon):
(city, state, country, lat, lon, timestamp))
conn.commit()
# get all student locations from database
def get_all_students(conn):
df = pd.read_sql_query("SELECT * from students", conn)
return df
# create map with student locations
def create_map(df):
# Create a map centered at the US
m = folium.Map(location=[41.2706, -97.1749], zoom_start=4)
# Group by city and count occurrences
@@ -104,6 +131,7 @@ def create_map(df):
max_count = city_counts['count'].max()
min_size, max_size = 5, 20 # min and max marker sizes
# Add markers for each city
for _, row in city_counts.iterrows():
# Create a custom icon
icon = CustomIcon(
@@ -112,7 +140,7 @@ def create_map(df):
icon_anchor=(15, 30), # Adjust anchor point if needed
popup_anchor=(0, -30) # Adjust popup anchor if needed
)
# Calculate marker size based on count
folium.Marker(
location=[row['latitude'], row['longitude']],
popup=f"{row['city']} <br> {row['count']}",

View File

@@ -3,7 +3,7 @@ from webpages import code_editor as ce
def pythonx_lesson1():
def pythonx_introduction():
st.title("Lesson 1: Introduction to Python")
# Lesson1-Part1: what is programming
st.header(":one: From Idea to Program")

View File

@@ -4,7 +4,7 @@ from wordcloud import WordCloud
import matplotlib.pyplot as plt
def pythonx_lesson2():
def pythonx_wordcloud():
st.title("Lesson 2: Create WordClouds in Python")
st.header(":one: What is WordClouds")
st.markdown("""