| 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, |
| ) |
|
|
| |
|
|
| @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) |
|
|
| |
| 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", {}) |
| |
| |
| 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", {}) |
| |
| |
| 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}") |
|
|
|
|