tarujain8's picture
feat: initialize FastAPI backend and Streamlit frontend for relationship analysis service
8bc63d1
Raw
History Blame Contribute Delete
14.9 kB
import time
import requests
import streamlit as st
from shared import MAX_FILES, analyze_payload, count_files, detect_perspective_payload
st.set_page_config(page_title="Velra | AI Relationship Intelligence", layout="centered", initial_sidebar_state="collapsed")
st.markdown(
"""
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Outfit:wght@300;400;500;600;700;800;900&display=swap');
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
header {visibility: hidden;}
[data-testid="stSidebar"] {display: none !important;}
[data-testid="collapsedControl"] {display: none !important;}
:root {
--primary: #FF3366;
--primary-glow: rgba(255, 51, 102, 0.4);
--secondary: #8B5CF6;
--secondary-glow: rgba(139, 92, 246, 0.4);
--bg-color: #FAFAFA;
--card-bg: rgba(255, 255, 255, 0.85);
--text-main: #111827;
--text-muted: #6B7280;
}
[data-testid="stAppViewContainer"] {
background-color: var(--bg-color);
background-image:
radial-gradient(circle at 15% 50%, rgba(255, 51, 102, 0.08), transparent 40%),
radial-gradient(circle at 85% 30%, rgba(139, 92, 246, 0.08), transparent 40%),
radial-gradient(circle at 50% 80%, rgba(236, 72, 153, 0.08), transparent 40%);
font-family: 'Inter', sans-serif;
}
.block-container {
max-width: 900px !important;
padding-top: 4rem !important;
padding-bottom: 4rem !important;
z-index: 10;
position: relative;
}
/* Ambient Orbs */
.orb {
position: fixed;
border-radius: 50%;
filter: blur(80px);
z-index: 0;
animation: float 20s infinite ease-in-out alternate;
opacity: 0.6;
pointer-events: none;
}
.orb-1 { width: 400px; height: 400px; background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 99%, #fecfef 100%); top: -100px; left: -100px; animation-delay: 0s; }
.orb-2 { width: 500px; height: 500px; background: linear-gradient(120deg, #e0c3fc 0%, #8ec5fc 100%); bottom: -150px; right: -100px; animation-delay: -5s; }
.orb-3 { width: 300px; height: 300px; background: linear-gradient(to top, #ff0844 0%, #ffb199 100%); top: 40%; left: 60%; animation-delay: -10s; opacity: 0.3; }
@keyframes float {
0% { transform: translate(0, 0) scale(1); }
50% { transform: translate(30px, -50px) scale(1.1); }
100% { transform: translate(-20px, 20px) scale(0.9); }
}
/* Typography */
.hero-title {
font-family: 'Outfit', sans-serif;
font-size: 4.5rem;
font-weight: 800;
line-height: 1.1;
letter-spacing: -0.03em;
color: var(--text-main);
text-align: center;
margin-bottom: 1rem;
background: linear-gradient(135deg, #111827 0%, #374151 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero-subtitle {
font-size: 1.25rem;
color: var(--text-muted);
text-align: center;
max-width: 650px;
margin: 0 auto 3rem auto;
line-height: 1.6;
font-weight: 400;
}
.gradient-text {
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
display: inline-block;
}
/* Cards and Inputs */
div[data-testid="stForm"] {
background: var(--card-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.6);
border-radius: 24px;
padding: 2.5rem;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.04), 0 1px 3px rgba(0,0,0,0.02), inset 0 0 0 1px rgba(255,255,255,0.5);
}
div[data-testid="stWidgetLabel"] p {
font-family: 'Outfit', sans-serif;
font-size: 1.1rem;
font-weight: 600;
color: var(--text-main);
margin-bottom: 0.5rem;
}
/* Input fields */
div[data-baseweb="input"], div[data-baseweb="textarea"], div[data-testid="stFileUploaderDropzone"] {
background: #ffffff !important;
border: 1px solid #E5E7EB !important;
border-radius: 16px !important;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
box-shadow: 0 2px 5px rgba(0,0,0,0.02) !important;
}
div[data-baseweb="input"]:focus-within, div[data-baseweb="textarea"]:focus-within, div[data-testid="stFileUploaderDropzone"]:hover {
border-color: var(--secondary) !important;
box-shadow: 0 0 0 4px var(--secondary-glow), 0 4px 12px rgba(0,0,0,0.05) !important;
background: #ffffff !important;
}
div[data-baseweb="input"] input, div[data-baseweb="textarea"] textarea, .stTextArea textarea, .stTextInput input {
background: transparent !important;
padding: 1rem 1.25rem !important;
font-size: 1rem !important;
color: #ffffff !important;
line-height: 1.6 !important;
}
div[data-baseweb="textarea"] textarea {
min-height: 150px !important;
}
/* Buttons */
div[data-testid="stFormSubmitButton"] button {
width: 100%;
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%) !important;
color: white !important;
border: none !important;
padding: 1rem 2rem !important;
font-family: 'Outfit', sans-serif !important;
font-size: 1.1rem !important;
font-weight: 600 !important;
border-radius: 16px !important;
margin-top: 1.5rem !important;
box-shadow: 0 10px 25px var(--primary-glow) !important;
transition: all 0.3s ease !important;
}
div[data-testid="stFormSubmitButton"] button:hover {
transform: translateY(-2px) !important;
box-shadow: 0 15px 35px var(--secondary-glow) !important;
}
/* Loading State */
.loading-container {
background: var(--card-bg);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.6);
border-radius: 24px;
padding: 3rem;
text-align: center;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.04);
margin-top: 2rem;
position: relative;
overflow: hidden;
}
.shimmer {
position: absolute;
top: 0; left: -100%; width: 50%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.4), transparent);
animation: shimmer 2s infinite;
}
@keyframes shimmer { 100% { left: 200%; } }
.loading-icon {
font-size: 3rem;
margin-bottom: 1rem;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.7; }
}
.loading-title {
font-family: 'Outfit', sans-serif;
font-size: 1.5rem;
font-weight: 700;
color: var(--text-main);
margin-bottom: 0.5rem;
}
.loading-text {
color: var(--text-muted);
font-size: 1.1rem;
}
/* Small Header */
.header-nav {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background: rgba(255,255,255,0.7);
backdrop-filter: blur(10px);
border-radius: 100px;
margin-bottom: 3rem;
border: 1px solid rgba(255,255,255,0.5);
box-shadow: 0 4px 15px rgba(0,0,0,0.03);
}
.header-logo {
font-family: 'Outfit', sans-serif;
font-weight: 800;
font-size: 1.5rem;
letter-spacing: -0.05em;
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
""",
unsafe_allow_html=True,
)
# Background Orbs
@st.dialog("Help Velra understand the emotional perspective")
def clarification_dialog():
st.markdown("<p style='color: #6B7280; margin-bottom: 1.5rem;'>Some conversations can be emotionally complex. A little context helps Velra give more accurate insights.</p>", unsafe_allow_html=True)
user_perspective = st.radio("Which side feels closest to your perspective?", ["Left side", "Right side", "Mixed / unsure"])
emotional_goal = st.selectbox("What are you hoping Velra helps you understand?", [
"Do they actually care?",
"Why are they emotionally distant?",
"Are we emotionally compatible?",
"Am I overthinking this?",
"Is this relationship healthy?",
"I just want clarity",
"Other"
])
if st.button("Continue Analysis ✨", use_container_width=True):
st.session_state.clarification_data = {
"user_perspective": user_perspective,
"emotional_goal": emotional_goal
}
st.session_state.show_dialog = False
st.session_state.run_analysis = True
st.rerun()
st.markdown('<div class="orb orb-1"></div><div class="orb orb-2"></div><div class="orb orb-3"></div>', unsafe_allow_html=True)
# Navigation
st.markdown(
"""
<div class="header-nav">
<div class="header-logo">Velra.</div>
<div style="font-weight: 500; color: #6B7280; font-size: 0.95rem;">Relationship Intelligence</div>
</div>
""", unsafe_allow_html=True
)
st.markdown(
"""
<h1 class="hero-title">See what they <br/><span class="gradient-text">REALLY mean.</span></h1>
<p class="hero-subtitle">Velra decodes emotional patterns, mixed signals, attachment dynamics, and relationship psychology. Upload your chat or explain the situation.</p>
""", unsafe_allow_html=True
)
col1, col2, col3 = st.columns([1, 2, 1])
default_chat = st.session_state.get("draft_chat", "")
default_feelings = st.session_state.get("draft_feelings", "")
with st.form("analysis_form", clear_on_submit=False):
chat = st.text_area(
"Conversation context",
value=default_chat,
placeholder="Paste messages, snippets, or simply explain what happened...",
)
feelings = st.text_input(
"Your intuition",
value=default_feelings,
placeholder="Describe this relationship briefly... Who is this person to you? What are you confused about?",
)
uploaded_files = st.file_uploader(
"Screenshots (Drag and Drop supported)",
accept_multiple_files=True,
help=f"Upload up to {MAX_FILES} chat screenshots.",
)
same_person = st.checkbox("Yes, all screenshots are from the same person/conversation", value=False)
submitted = st.form_submit_button("Analyze Connection ✨", use_container_width=True)
if submitted:
if count_files(uploaded_files) > MAX_FILES:
st.error(f"Please limit uploads to {MAX_FILES} images.")
elif count_files(uploaded_files) > 0 and not chat.strip() and not feelings.strip():
st.error("Since you only uploaded images without a chat transcript, your feelings/intuition are REQUIRED.")
elif count_files(uploaded_files) > 0 and not same_person:
st.error("Please confirm that all screenshots are from the same person/conversation.")
else:
with st.spinner("Detecting perspective..."):
try:
res = detect_perspective_payload(chat, feelings, uploaded_files)
needs_clar = res.get("needs_clarification", False) or res.get("confidence", 1.0) < 0.75 or res.get("possible_multiple_conversations", False)
except Exception as e:
needs_clar = True
if needs_clar:
st.session_state.show_dialog = True
st.session_state.pending_chat = chat
st.session_state.pending_feelings = feelings
st.session_state.pending_files = uploaded_files
st.rerun()
else:
st.session_state.run_analysis = True
st.session_state.clarification_data = {}
st.session_state.pending_chat = chat
st.session_state.pending_feelings = feelings
st.session_state.pending_files = uploaded_files
st.rerun()
if st.session_state.get("show_dialog"):
clarification_dialog()
if st.session_state.get("run_analysis"):
loading_placeholder = st.empty()
loading_html = """
<div class="loading-container">
<div class="shimmer"></div>
<div class="loading-icon">✨</div>
<div class="loading-title">Decoding emotional signals...</div>
<div class="loading-text" id="loading-stage">Reading conversational tone</div>
</div>
"""
loading_placeholder.markdown(loading_html, unsafe_allow_html=True)
stages = [
"Detecting attachment signals...",
"Measuring emotional consistency...",
"Identifying mixed signals...",
"Predicting relationship trajectory..."
]
for stage in stages:
time.sleep(0.8)
loading_placeholder.markdown(
f"""
<div class="loading-container">
<div class="shimmer"></div>
<div class="loading-icon">🧠</div>
<div class="loading-title">Analyzing dynamics...</div>
<div class="loading-text">{stage}</div>
</div>
""", unsafe_allow_html=True
)
try:
c_data = st.session_state.get("clarification_data", {})
# Use values from session state to avoid empty inputs after rerun
analysis_chat = st.session_state.get("pending_chat", "")
analysis_feelings = st.session_state.get("pending_feelings", "")
analysis_files = st.session_state.get("pending_files", [])
payload = analyze_payload(
analysis_chat,
analysis_feelings,
analysis_files,
user_perspective=c_data.get("user_perspective", ""),
emotional_goal=c_data.get("emotional_goal", ""),
conversation_consistency=c_data.get("conversation_consistency", "")
)
st.session_state.run_analysis = False
st.session_state.show_dialog = False
st.session_state["analysis_payload"] = payload.get("analysis", {})
st.session_state["analysis_raw"] = payload.get("raw", {})
# Save the actual text used for analysis to drafts
st.session_state["draft_chat"] = analysis_chat
st.session_state["draft_feelings"] = analysis_feelings
loading_placeholder.empty()
st.switch_page("pages/results.py")
except Exception as e:
loading_placeholder.empty()
st.error(f"Velra encountered an issue connecting to the engine. Details: {e}")