jostlebot's picture
Add prototype context to front page
8ad692f
"""Practice Fields Hub β€” Curated Therapeutic Practice Spaces"""
import streamlit as st
import base64
import os
st.set_page_config(page_title="Practice Fields", page_icon="🌱", layout="wide")
def get_image_base64(image_path):
"""Convert image to base64 for embedding in HTML."""
try:
with open(image_path, "rb") as f:
return base64.b64encode(f.read()).decode()
except:
return None
# Get base path for images
BASE_PATH = os.path.dirname(os.path.abspath(__file__))
IMG_PATH = os.path.join(BASE_PATH, "images")
st.markdown("""
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap');
.stApp {
background: linear-gradient(160deg, #e8f4f8 0%, #f0faf0 30%, #fdfbf4 60%, #f8f4ff 100%);
font-family: 'Inter', sans-serif;
}
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
.hub-header {
text-align: center;
padding: 3rem 2rem 2rem 2rem;
margin-bottom: 1rem;
}
.hub-logo {
font-size: 4rem;
margin-bottom: 1rem;
filter: drop-shadow(0 4px 8px rgba(100, 160, 140, 0.2));
}
.hub-title {
color: #3a5050;
font-size: 2.8rem;
font-weight: 300;
letter-spacing: -0.02em;
margin-bottom: 0.75rem;
}
.hub-tagline {
color: #6a8080;
font-size: 1.15rem;
font-weight: 300;
font-style: italic;
}
.clinician-badge {
background: linear-gradient(135deg, rgba(255,255,255,0.9) 0%, rgba(240,250,245,0.9) 100%);
border-radius: 20px;
padding: 1.25rem 2rem;
text-align: center;
margin: 1.5rem auto 2rem auto;
max-width: 450px;
border: 1px solid rgba(160, 200, 180, 0.3);
box-shadow: 0 4px 20px rgba(100, 160, 140, 0.1);
}
.clinician-name {
color: #3a5050;
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.25rem;
}
.clinician-orientation {
color: #7a9a9a;
font-size: 0.95rem;
font-weight: 300;
}
.field-card {
background: linear-gradient(145deg, rgba(255,255,255,0.95) 0%, rgba(252,255,253,0.95) 100%);
border-radius: 20px;
padding: 1.5rem;
margin: 0.6rem 0;
border: 1px solid rgba(180, 210, 195, 0.25);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
text-decoration: none;
display: block;
color: inherit;
box-shadow: 0 2px 12px rgba(100, 140, 130, 0.06);
}
.field-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 32px rgba(100, 160, 140, 0.15);
border-color: rgba(140, 190, 170, 0.4);
}
.field-icon {
width: 80px;
height: 80px;
object-fit: contain;
margin-bottom: 0.75rem;
border-radius: 12px;
}
.field-title {
color: #3a5555;
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
}
.field-desc {
color: #6a8585;
font-size: 0.9rem;
line-height: 1.5;
font-weight: 300;
}
.section-header {
color: #5a7a7a;
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.15em;
font-weight: 500;
margin: 2.5rem 0 1.5rem 0;
padding-bottom: 0.75rem;
border-bottom: 2px solid rgba(160, 200, 180, 0.2);
}
.footer-container {
text-align: center;
padding: 3rem 2rem 2rem 2rem;
margin-top: 3rem;
background: linear-gradient(180deg, transparent 0%, rgba(230, 245, 240, 0.3) 100%);
border-top: 1px solid rgba(180, 210, 195, 0.2);
}
.footer-main {
color: #6a8a8a;
font-size: 0.95rem;
font-weight: 300;
margin-bottom: 1rem;
line-height: 1.6;
}
.crisis-resources {
display: inline-flex;
gap: 1.5rem;
background: rgba(255,255,255,0.6);
padding: 0.75rem 1.5rem;
border-radius: 30px;
}
.crisis-item {
color: #5a7a7a;
font-size: 0.9rem;
font-weight: 500;
}
.welcome-text {
color: #4a6a6a;
font-size: 1.1rem;
font-weight: 300;
text-align: center;
margin-bottom: 2rem;
line-height: 1.6;
}
.stTextInput > div > div > input {
background-color: rgba(255,255,255,0.9) !important;
border: 1px solid rgba(180, 210, 195, 0.4) !important;
border-radius: 12px !important;
padding: 0.75rem 1rem !important;
}
.stTextArea > div > div > textarea {
background-color: rgba(255,255,255,0.9) !important;
border: 1px solid rgba(180, 210, 195, 0.4) !important;
border-radius: 12px !important;
}
.stButton > button {
background: linear-gradient(135deg, #7eb8a8 0%, #6aaa98 100%) !important;
border: none !important;
border-radius: 12px !important;
color: white !important;
font-weight: 500 !important;
padding: 0.75rem 2rem !important;
box-shadow: 0 4px 12px rgba(100, 160, 140, 0.25) !important;
}
.stButton > button:hover {
background: linear-gradient(135deg, #6aaa98 0%, #5a9a88 100%) !important;
transform: translateY(-2px) !important;
}
.divider {
height: 1px;
background: linear-gradient(90deg, transparent, rgba(160, 200, 180, 0.3), transparent);
margin: 2rem 0;
}
</style>
""", unsafe_allow_html=True)
if "clinician_name" not in st.session_state:
st.session_state.clinician_name = ""
if "clinician_orientation" not in st.session_state:
st.session_state.clinician_orientation = ""
if "onboarded" not in st.session_state:
st.session_state.onboarded = False
# Load images
IMAGES = {
"shadowbox": get_image_base64(os.path.join(IMG_PATH, "shadowbox.webp")),
"tendsend": get_image_base64(os.path.join(IMG_PATH, "tendsend.webp")),
"buildabot": get_image_base64(os.path.join(IMG_PATH, "buildabot.webp")),
"diagnosis": get_image_base64(os.path.join(IMG_PATH, "diagnosis.webp")),
"gspt": get_image_base64(os.path.join(IMG_PATH, "gspt.webp")),
"learnnvc": get_image_base64(os.path.join(IMG_PATH, "learnnvc.webp")),
"difficultconversations": get_image_base64(os.path.join(IMG_PATH, "difficultconversations.webp")),
}
def img_tag(key):
if IMAGES.get(key):
return f'<img src="data:image/webp;base64,{IMAGES[key]}" class="field-icon" />'
return ""
SPACES = [
{"key": "shadowbox", "title": "ShadowBox Library", "desc": "A resonant library for hard thoughts. Psychoeducation without synthetic intimacy.", "url": "https://huggingface.co/spaces/jostlebot/shadowbox.library"},
{"key": "tendsend", "title": "Tend & Send", "desc": "Craft messages with care. Practice tending to yourself before sending.", "url": "https://huggingface.co/spaces/jostlebot/TendSend.PrototypeBot"},
{"key": "buildabot", "title": "Build a Bot", "desc": "AI literacy β€” understand how LLMs work, spot synthetic intimacy.", "url": "https://huggingface.co/spaces/jostlebot/BuildABot.2"},
{"key": "diagnosis", "title": "Diagnosis Explorer", "desc": "Understand diagnostic categories with nuance and care.", "url": "https://huggingface.co/spaces/jostlebot/DiagnosisExplorer"},
{"key": "gspt", "title": "GSPT", "desc": "Generating Safer Passages of Text. Warm, boundaried reflections.", "url": "https://huggingface.co/spaces/jostlebot/GSPT"},
{"key": "learnnvc", "title": "Learn NVC", "desc": "Practice Nonviolent Communication. Transform judgments into needs.", "url": "https://huggingface.co/spaces/jostlebot/LearnNVC"},
{"key": "difficultconversations", "title": "Difficult Conversations", "desc": "Rehearse hard conversations before having them for real.", "url": "https://huggingface.co/spaces/jostlebot/PracticeDifficultConversations"},
]
if not st.session_state.onboarded:
st.markdown("""
<div class="hub-header">
<div class="hub-logo">🌱</div>
<div class="hub-title">Practice Fields</div>
<div class="hub-tagline">Example Prototype</div>
</div>
""", unsafe_allow_html=True)
st.markdown("""
<div style="max-width: 700px; margin: 0 auto 2rem auto; text-align: center; padding: 0 1rem;">
<p style="color: #4a6a6a; font-size: 1.1rem; line-height: 1.7; font-weight: 300;">
This is a <strong>prototype demonstration</strong> of what becomes possible when mental health professionals
bring clinical insight, experience, and wisdom to the design of AI-assisted tools.
</p>
<p style="color: #6a8a8a; font-size: 1rem; line-height: 1.7; font-weight: 300; margin-top: 1rem;">
Each space represents <strong>bounded, trauma-informed innovation</strong> β€”
using LLMs as a relational medium with extraordinary strategic care.
Not replacing therapy. Not performing synthetic intimacy.
Building bridges back to human connection.
</p>
<p style="color: #7a9a9a; font-size: 0.95rem; line-height: 1.6; font-weight: 300; margin-top: 1.5rem; font-style: italic;">
Created by a licensed clinician exploring what ethical, clinically-grounded AI practice spaces can look like.
</p>
</div>
""", unsafe_allow_html=True)
st.markdown('<div class="divider"></div>', unsafe_allow_html=True)
st.markdown("""
<div class="welcome-text">
Enter as Clinician<br>
<span style="font-size: 0.95rem; color: #7a9a9a;">Personalize this hub to explore the prototype spaces</span>
</div>
""", unsafe_allow_html=True)
col1, col2, col3 = st.columns([1, 2, 1])
with col2:
with st.form("onboarding"):
name = st.text_input("Your Name & Credentials", placeholder="e.g., Dr. Sarah Chen, LMHC")
orientation = st.text_area("Clinical Orientation", placeholder="e.g., Attachment-focused, DBT-informed, IFS, somatic...", height=80)
st.markdown("<br>", unsafe_allow_html=True)
if st.form_submit_button("Enter Practice Fields", use_container_width=True):
if name:
st.session_state.clinician_name = name
st.session_state.clinician_orientation = orientation
st.session_state.onboarded = True
st.rerun()
else:
st.error("Please enter your name.")
else:
st.markdown("""
<div class="hub-header">
<div class="hub-logo">🌱</div>
<div class="hub-title">Practice Fields</div>
<div class="hub-tagline">Structured practice for relational growth</div>
</div>
""", unsafe_allow_html=True)
st.markdown(f"""
<div class="clinician-badge">
<div class="clinician-name">{st.session_state.clinician_name}</div>
<div class="clinician-orientation">{st.session_state.clinician_orientation or "Clinical Practice"}</div>
</div>
""", unsafe_allow_html=True)
col1, col2, col3 = st.columns([1, 1, 1])
with col2:
if st.button("✎ Edit Info", use_container_width=True):
st.session_state.onboarded = False
st.rerun()
st.markdown('<p class="section-header">Practice Spaces</p>', unsafe_allow_html=True)
col1, col2 = st.columns(2, gap="medium")
for i, s in enumerate(SPACES):
with col1 if i % 2 == 0 else col2:
st.markdown(f"""
<a href="{s["url"]}" target="_blank" class="field-card">
{img_tag(s["key"])}
<div class="field-title">{s["title"]}</div>
<div class="field-desc">{s["desc"]}</div>
</a>
""", unsafe_allow_html=True)
st.markdown("""
<div class="footer-container">
<div class="footer-main">
Not therapy. Not a replacement for human connection.<br>
Practice spaces designed to complement work with your therapist.
</div>
<div class="crisis-resources">
<span class="crisis-item">988</span>
<span class="crisis-item">741741</span>
<span class="crisis-item">911</span>
</div>
</div>
""", unsafe_allow_html=True)