Spaces:
Sleeping
Sleeping
| 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(" β’ ".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) | |