tharu280's picture
Initial commit
bc620e9
import os
import json
import requests
import streamlit as st
# --- Configuration ---
API_URL = "http://127.0.0.1:8000/chat"
IMAGES_DIR = "images"
# --- Page Setup ---
st.set_page_config(
page_title="Tharushika | AI Portfolio",
page_icon="πŸ‘‹",
layout="centered",
initial_sidebar_state="collapsed"
)
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
html, body, .stApp {
background-color: #ffffff !important;
font-family: -apple-system, BlinkMacSystemFont, 'Inter', sans-serif !important;
color: #1d1d1f !important;
}
p, .stMarkdown, .stText {
color: #333333;
line-height: 1.6;
}
h1, h2, h3, h4, h5, h6 {
font-weight: 600 !important;
letter-spacing: -0.02em !important;
color: #1d1d1f !important;
}
h1 { font-size: 2.8rem !important; margin-bottom: 0.5rem !important;}
h2 { font-size: 2.2rem !important; margin-top: 0.5rem !important;}
h3 { font-size: 1.5rem !important; margin-top: 2rem !important; margin-bottom: 1rem !important;}
h4 { font-size: 1.2rem !important; margin-top: 1rem !important;}
.centered-title h1, .centered-title h2 {
text-align: center;
width: 100%;
}
.centered-title h2 {
color: #6e6e73 !important;
font-weight: 500 !important;
font-size: 1.8rem !important;
}
.centered-profile-pic {
display: flex;
justify-content: center;
margin-top: 2rem;
margin-bottom: 2rem;
}
.centered-profile-pic img {
border-radius: 20px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
border: 1px solid rgba(0,0,0,0.05);
}
.stChatMessage {
background-color: transparent !important;
border: none !important;
padding: 0.8rem 0 !important;
}
.stChatMessage [data-testid="stChatMessageContent"] {
background-color: #f5f5f7 !important;
border-radius: 18px !important;
padding: 0.8rem 1.2rem !important;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
color: #1d1d1f !important;
font-size: 1.0rem !important;
line-height: 1.5 !important;
}
.stChatMessage[data-testid="user-message"] [data-testid="stChatMessageContent"] {
background-color: #0071e3 !important;
color: white !important;
}
div[data-testid="stVerticalBlock"] > [style*="flex-direction: column;"] > [data-testid="stVerticalBlock"] {
background-color: #ffffff;
border: 1px solid rgba(0,0,0,0.08);
border-radius: 16px !important;
padding: 20px !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.03);
transition: transform 0.2s ease, box-shadow 0.2s ease;
margin-bottom: 15px;
}
div[data-testid="stVerticalBlock"] > [style*="flex-direction: column;"] > [data-testid="stVerticalBlock"]:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(0,0,0,0.08);
border-color: rgba(0,0,0,0.15);
}
.stButton > button {
border-radius: 12px !important;
font-weight: 500 !important;
border: 1px solid #d2d2d7 !important;
background-color: #ffffff !important;
color: #1d1d1f !important;
padding: 0.6rem 1rem !important;
transition: all 0.2s !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
width: 100%;
}
.stButton > button:hover {
background-color: #f5f5f7 !important;
border-color: #c0c0c5 !important;
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
.stTextInput input {
border-radius: 12px !important;
border: 1px solid #d2d2d7 !important;
padding: 12px 15px !important;
font-size: 1rem !important;
background-color: rgba(255,255,255,0.8) !important;
backdrop-filter: blur(10px);
color: #1d1d1f !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.stTextInput input:focus {
border-color: #0071e3 !important;
box-shadow: 0 0 0 4px rgba(0,113,227,0.15) !important;
}
div.stChatInputContainer {
padding-top: 15px;
background-color: #ffffff;
padding-bottom: 1rem;
}
a { color: #0071e3 !important; text-decoration: none !important; }
a:hover { text-decoration: underline !important; }
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
header {visibility: hidden;}
.quick-action-button .stButton > button {
border-radius: 999px !important;
padding: 0.8rem 1.5rem !important;
width: auto;
}
.quick-action-button {
display: flex;
justify-content: center;
margin-top: 2rem;
gap: 15px;
flex-wrap: wrap;
}
</style>
""", unsafe_allow_html=True)
# --- Helper Functions ---
def render_projects(data):
st.markdown("### Featured Projects")
if not data:
st.info("No projects data received.")
return
cols = st.columns(2)
for i, proj in enumerate(data):
with cols[i % 2]:
with st.container(border=True):
img_path = proj.get("image_path", "")
if img_path and os.path.exists(img_path):
st.image(img_path, use_container_width=True)
else:
st.markdown(f"""<div style='height:140px; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); border-radius: 12px; display:flex; align-items:center; justify-content:center; color:#666;'>No Image</div>""", unsafe_allow_html=True)
st.markdown(f"#### {proj.get('title', 'Untitled')}")
st.caption(proj.get('type', 'Project').upper())
with st.expander("View Details"):
st.write(proj.get('description', ''))
st.markdown(
f"**Tech Stack:** {proj.get('technologies', '')}")
links = []
if proj.get('github_url'):
links.append(f"[GitHub]({proj.get('github_url')})")
if proj.get('demo_url'):
links.append(f"[Live Demo]({proj.get('demo_url')})")
if links:
st.markdown(" &nbsp;β€’&nbsp; ".join(links))
def render_skills(data):
st.markdown("### Skills & Expertise")
if not data:
st.info("No skills data received.")
return
for category, skills in data.items():
with st.container(border=True):
st.markdown(f"**{category}**")
badges = "".join(
[f"<span style='background:#f5f5f7; padding:4px 10px; border-radius:12px; margin:0 5px 5px 0; display:inline-block; font-size:0.85rem;'>{s}</span>" for s in skills])
st.markdown(badges, unsafe_allow_html=True)
def render_articles(data):
st.markdown("### Articles")
if not data:
st.info("No articles found.")
return
for item in data:
with st.container(border=True):
st.markdown(f"**{item.get('title', 'Untitled')}**")
st.markdown(
f"<p style='color:#666; font-size:0.9rem;'>{item.get('description', '')}</p>", unsafe_allow_html=True)
if item.get('url'):
st.markdown(f"[Read Article β€Ί]({item['url']})")
def render_videos(data):
st.markdown("### Video Tutorials")
if not data:
st.info("No videos found.")
return
cols = st.columns(2)
for i, item in enumerate(data):
with cols[i % 2]:
with st.container(border=True):
thumb = item.get('thumbnail_url', "")
if thumb and os.path.exists(thumb):
st.image(thumb, use_container_width=True)
st.markdown(f"**{item.get('title', 'Untitled')}**")
st.markdown(
f"<p style='color:#666; font-size:0.9rem;'>{item.get('description', '')}</p>", unsafe_allow_html=True)
if item.get('url'):
st.markdown(f"[Watch on YouTube β€Ί]({item['url']})")
def render_research(data):
st.markdown("### Research")
if not data:
st.info("No research found.")
return
for item in data:
with st.container(border=True):
st.markdown(f"**{item.get('title', 'Untitled')}**")
st.markdown(
f"<p style='color:#666; font-size:0.9rem;'>{item.get('description', '')}</p>", unsafe_allow_html=True)
if item.get('url'):
st.markdown(f"[View Publication β€Ί]({item['url']})")
def render_certifications(data):
st.markdown("### Certifications")
if not data:
st.info("No certifications found.")
return
for item in data:
st.markdown(f"""
<div style='display:flex; align-items:center; margin-bottom:10px;'>
<span style='font-size:1.2rem; margin-right:10px;'>πŸŽ–οΈ</span>
<span style='font-size:1rem; font-weight:500;'>{item}</span>
</div>
""", unsafe_allow_html=True)
# --- NEW: Resume Renderer ---
def render_resume(data):
st.markdown("### πŸ“„ Resume / CV")
col1, col2 = st.columns([1, 2])
with col1:
preview_path = data.get("preview_image", "")
if preview_path and os.path.exists(preview_path):
st.image(preview_path, caption="Preview", use_container_width=True)
else:
st.markdown("""
<div style="height: 200px; background-color: #f5f5f7; border-radius: 12px; display: flex; align-items: center; justify-content: center;">
<span style="font-size: 3rem;">πŸ“„</span>
</div>
""", unsafe_allow_html=True)
with col2:
st.markdown(f"#### {data.get('title', 'Resume')}")
st.write(data.get('description', ''))
pdf_path = data.get("file_path", "")
if pdf_path and os.path.exists(pdf_path):
with open(pdf_path, "rb") as pdf_file:
pdf_bytes = pdf_file.read()
st.download_button(
label="πŸ“₯ Download Resume (PDF)",
data=pdf_bytes,
file_name="Tharushika_Abedheera_Resume.pdf",
mime="application/pdf",
)
else:
st.error("Resume file not found.")
def render_content(data):
st.markdown("### Content & Research")
if not data:
return
tab1, tab2, tab3 = st.tabs(["Articles", "Videos", "Research"])
with tab1:
render_articles(data.get('articles', []))
with tab2:
render_videos(data.get('videos', []))
with tab3:
render_research(data.get('research', []))
# --- Centralized Chat Logic Function ---
def process_chat_message(prompt):
with st.spinner("Processing..."):
try:
response = requests.post(API_URL, json={"message": prompt})
if response.status_code == 200:
api_data = response.json()
st.session_state.last_exchange = {
"user_query": prompt,
"ai_response": api_data.get("response", ""),
"tool_code": api_data.get("tool_code"),
"tool_data": api_data.get("tool_data")
}
else:
st.error(f"Backend Error: {response.status_code}")
except Exception as e:
st.error(f"Connection Failed: {e}")
st.rerun()
# --- Main Layout ---
if "last_exchange" not in st.session_state:
st.session_state.last_exchange = None
# --- Top Section: Profile and Introduction ---
if not st.session_state.last_exchange:
st.markdown("<div class='centered-profile-pic'>", unsafe_allow_html=True)
profile_pic_path = "images/profile.png"
if os.path.exists(profile_pic_path):
st.image(profile_pic_path, width=160)
else:
st.markdown(f"""<div style='height:160px; width:160px; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); border-radius: 20px; display:flex; align-items:center; justify-content:center; color:#666; font-size:0.8rem; margin: 0 auto;'>Add profile.png</div>""", unsafe_allow_html=True)
st.markdown("</div>", unsafe_allow_html=True)
st.markdown("<div class='centered-title'>", unsafe_allow_html=True)
st.markdown("<h1>Hey, I'm Tharushika πŸ‘‹</h1>", unsafe_allow_html=True)
st.markdown("<h2>Machine Learning Engineer</h2>", unsafe_allow_html=True)
st.markdown("</div>", unsafe_allow_html=True)
st.markdown("<div class='quick-action-button'>", unsafe_allow_html=True)
if st.button("Me"):
process_chat_message("Tell me about yourself")
if st.button("Projects"):
process_chat_message("Show me your projects")
if st.button("Skills"):
process_chat_message("What are your skills?")
if st.button("Contact"):
process_chat_message("How can I contact you?")
st.markdown("</div>", unsafe_allow_html=True)
if prompt := st.chat_input("Ask me anything..."):
process_chat_message(prompt)
# --- Conversation Area ---
if st.session_state.last_exchange:
exchange = st.session_state.last_exchange
with st.chat_message("user"):
st.write(exchange["user_query"])
with st.chat_message("assistant"):
st.write(exchange["ai_response"])
tool_code = exchange.get("tool_code")
tool_data = exchange.get("tool_data")
if tool_code == "show_projects":
render_projects(tool_data)
elif tool_code == "show_skills":
render_skills(tool_data)
elif tool_code == "show_content":
render_content(tool_data)
elif tool_code == "show_videos":
render_videos(tool_data)
elif tool_code == "show_articles":
render_articles(tool_data)
elif tool_code == "show_research":
render_research(tool_data)
elif tool_code == "show_certifications":
render_certifications(tool_data)
elif tool_code == "show_resume":
render_resume(tool_data) # <--- RESUME HANDLER ADDED
if prompt := st.chat_input("Ask for more details..."):
process_chat_message(prompt)