""" Raven AI — Main Application Full-featured emotionally-aware AI assistant with streaming, multi-chat, document processing, image understanding, voice input, data analysis, and web search. """ import json import os import uuid import time import streamlit as st import pandas as pd import matplotlib.pyplot as plt import matplotlib matplotlib.use("Agg") from emotion_engine import detect_emotion, detect_intent, detect_crisis from responder import ( get_response, get_response_stream, generate_title, analyze_image, transcribe_audio, process_document, analyze_data, search_and_respond, ) # ── Page config ────────────────────────────────────────────────────────────── st.set_page_config( page_title="Raven AI", page_icon="🐦‍⬛", layout="wide", initial_sidebar_state="expanded", ) # ── Conversation persistence file ──────────────────────────────────────────── CONV_FILE = os.path.join(os.path.dirname(__file__), ".raven_conversations.json") def save_conversations(): """Save all conversations to disk for persistence.""" try: data = {} for cid, conv in st.session_state.conversations.items(): data[cid] = { "title": conv.get("title", "New Chat"), "messages": conv.get("messages", []), "emotion_log": conv.get("emotion_log", []), "current_emotion": conv.get("current_emotion", "neutral"), "current_intent": conv.get("current_intent", "casual"), "created_at": conv.get("created_at", time.time()), } with open(CONV_FILE, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) except Exception: pass def load_conversations(): """Load conversations from disk.""" try: if os.path.exists(CONV_FILE): with open(CONV_FILE, "r", encoding="utf-8") as f: return json.load(f) except Exception: pass return {} # ── Custom CSS ─────────────────────────────────────────────────────────────── st.markdown(""" """, unsafe_allow_html=True) # ── Style maps ─────────────────────────────────────────────────────────────── EMOTION_STYLE = { "happy": {"emoji": "😊", "color": "#F59E0B"}, "sad": {"emoji": "😔", "color": "#60A5FA"}, "anxious": {"emoji": "😰", "color": "#A78BFA"}, "angry": {"emoji": "😤", "color": "#F87171"}, "confused": {"emoji": "😕", "color": "#34D399"}, "neutral": {"emoji": "😐", "color": "#9CA3AF"}, } INTENT_STYLE = { "venting": {"emoji": "💬", "label": "Venting"}, "question": {"emoji": "🔍", "label": "Question"}, "advice": {"emoji": "💡", "label": "Advice"}, "casual": {"emoji": "😌", "label": "Casual"}, "crisis": {"emoji": "🆘", "label": "Crisis"}, "coding": {"emoji": "💻", "label": "Coding"}, "learning": {"emoji": "📚", "label": "Learning"}, "creative": {"emoji": "🎨", "label": "Creative"}, "deep_dive": {"emoji": "🔬", "label": "Deep Dive"}, } EMOTION_ORDER = ["happy", "neutral", "confused", "anxious", "sad", "angry"] EMOTION_COLORS = { "happy": "#F59E0B", "sad": "#60A5FA", "anxious": "#A78BFA", "angry": "#F87171", "confused": "#34D399", "neutral": "#9CA3AF", } WELCOME_PROMPTS = [ {"emoji": "💻", "title": "Write Code", "prompt": "Write a Python script that scrapes the top 10 trending GitHub repos"}, {"emoji": "📚", "title": "Learn Something", "prompt": "Explain how neural networks work — from scratch, step by step"}, {"emoji": "🎨", "title": "Creative Writing", "prompt": "Write a short sci-fi story about an AI that develops emotions"}, {"emoji": "🧩", "title": "Solve a Problem", "prompt": "I need to build a portfolio website. Help me plan the architecture and tech stack"}, {"emoji": "📊", "title": "Analyze Data", "prompt": "Explain the key concepts of statistics I need for data science"}, {"emoji": "🔬", "title": "Deep Dive", "prompt": "Give me an exhaustive breakdown of how transformers work in AI"}, ] # ═══════════════════════════════════════════════════════════════════════════════ # SESSION STATE # ═══════════════════════════════════════════════════════════════════════════════ def create_conversation(): """Create a new conversation and return its ID.""" conv_id = str(uuid.uuid4()) st.session_state.conversations[conv_id] = { "title": "New Chat", "messages": [], "emotion_log": [], "current_emotion": "neutral", "current_intent": "casual", "created_at": time.time(), } return conv_id def get_conv(): """Get the active conversation dict.""" return st.session_state.conversations.get(st.session_state.active_conv_id, {}) def delete_conversation(conv_id): """Delete a conversation.""" if conv_id in st.session_state.conversations: del st.session_state.conversations[conv_id] # If we deleted the active one, switch to another or create new if conv_id == st.session_state.active_conv_id: remaining = list(st.session_state.conversations.keys()) if remaining: st.session_state.active_conv_id = remaining[0] else: new_id = create_conversation() st.session_state.active_conv_id = new_id save_conversations() def init_session_state(): """Initialize all session state variables.""" if "conversations" not in st.session_state: loaded = load_conversations() if loaded: st.session_state.conversations = loaded else: st.session_state.conversations = {} if "active_conv_id" not in st.session_state: # Always start with a fresh new chat — old chats stay in sidebar conv_id = create_conversation() st.session_state.active_conv_id = conv_id # Ensure active conv exists if st.session_state.active_conv_id not in st.session_state.conversations: conv_id = create_conversation() st.session_state.active_conv_id = conv_id if "pending_prompt" not in st.session_state: st.session_state.pending_prompt = None if "regenerate_idx" not in st.session_state: st.session_state.regenerate_idx = None if "uploaded_context" not in st.session_state: st.session_state.uploaded_context = None if "preferences" not in st.session_state: st.session_state.preferences = {"tone": "balanced", "name": ""} if "voice_transcript" not in st.session_state: st.session_state.voice_transcript = None if "edit_msg_idx" not in st.session_state: st.session_state.edit_msg_idx = None if "upload_key" not in st.session_state: st.session_state.upload_key = 0 init_session_state() # ═══════════════════════════════════════════════════════════════════════════════ # HELPER FUNCTIONS # ═══════════════════════════════════════════════════════════════════════════════ def build_emotion_chart(emotion_log): """Build the emotion trend chart.""" if len(emotion_log) < 2: return None emotions = [e for _, e in emotion_log] y_vals = [EMOTION_ORDER.index(e) if e in EMOTION_ORDER else 2 for e in emotions] x_vals = list(range(1, len(emotions) + 1)) colors = [EMOTION_COLORS.get(e, "#9CA3AF") for e in emotions] fig, ax = plt.subplots(figsize=(3.5, 2.2)) fig.patch.set_facecolor("#0e1117") ax.set_facecolor("#0e1117") ax.plot(x_vals, y_vals, color="#6366F1", linewidth=1.5, alpha=0.6, zorder=1) for x, y, c in zip(x_vals, y_vals, colors): ax.scatter(x, y, color=c, s=50, zorder=2) ax.set_yticks(range(len(EMOTION_ORDER))) ax.set_yticklabels(EMOTION_ORDER, fontsize=7, color="#9CA3AF") ax.set_xticks(x_vals[-10:]) ax.set_xticklabels([str(i) for i in x_vals[-10:]], fontsize=7, color="#9CA3AF") ax.set_xlabel("Message", fontsize=7, color="#9CA3AF") ax.tick_params(colors="#9CA3AF") for spine in ax.spines.values(): spine.set_edgecolor("#2d2d3a") ax.grid(axis="y", color="#2d2d3a", linewidth=0.5, linestyle="--") plt.tight_layout(pad=0.5) return fig def get_recent_context(): """Get recent user messages for context.""" conv = get_conv() user_msgs = [m["content"] for m in conv.get("messages", []) if m.get("role") == "user"][-2:] return " | ".join(user_msgs) if user_msgs else "" def render_badges(emotion, intent): """Render emotion + intent badges.""" s = EMOTION_STYLE.get(emotion, EMOTION_STYLE["neutral"]) i = INTENT_STYLE.get(intent, INTENT_STYLE["casual"]) st.markdown( f"" f"{s['emoji']} {emotion}" f"" f"{i['emoji']} {i['label']}", unsafe_allow_html=True, ) def render_word_count(text): """Show word count below a response.""" words = len(text.split()) st.markdown(f"
{words} words
", unsafe_allow_html=True) def process_uploaded_file(uploaded_file): """Process an uploaded file and return (content_text, file_type).""" name = uploaded_file.name.lower() raw = uploaded_file.read() if name.endswith((".png", ".jpg", ".jpeg", ".gif", ".webp")): return raw, "image" if name.endswith((".wav", ".mp3", ".m4a", ".ogg", ".flac", ".webm")): return raw, "audio" if name.endswith((".csv", ".tsv")): try: sep = "\t" if name.endswith(".tsv") else "," uploaded_file.seek(0) df = pd.read_csv(uploaded_file, sep=sep) summary = f"Shape: {df.shape}\n\nColumns: {list(df.columns)}\n\nFirst 10 rows:\n{df.head(10).to_string()}\n\nDescribe:\n{df.describe().to_string()}" return summary, "data" except Exception: return raw.decode("utf-8", errors="ignore"), "text" if name.endswith(".pdf"): try: import PyPDF2 uploaded_file.seek(0) reader = PyPDF2.PdfReader(uploaded_file) text = "" for page in reader.pages[:50]: text += page.extract_text() or "" return text, "document" except Exception: return "Could not read PDF.", "error" try: text = raw.decode("utf-8", errors="ignore") return text, "document" except Exception: return "Could not read file.", "error" # ═══════════════════════════════════════════════════════════════════════════════ # SIDEBAR — Clean layout: New Chat, Conversations, Mood, Preferences, Graph # ═══════════════════════════════════════════════════════════════════════════════ with st.sidebar: # ── i) Header + New Chat ───────────────────────────────────────────────── st.markdown( "

🐦‍⬛ Raven AI

" "

" "Thought. Memory. Empathy.

", unsafe_allow_html=True, ) if st.button("➕ New Chat", use_container_width=True, type="primary"): conv_id = create_conversation() st.session_state.active_conv_id = conv_id st.session_state.uploaded_context = None st.session_state.upload_key += 1 save_conversations() st.rerun() st.divider() # ── ii) Past Conversations (with 3-dot menu each) ──────────────────────── convs = sorted( st.session_state.conversations.items(), key=lambda x: x[1].get("created_at", 0), reverse=True, ) # Filter: show chats that have messages, plus the active one visible_convs = [ (cid, c) for cid, c in convs if len([m for m in c.get("messages", []) if m.get("role") == "user"]) > 0 or cid == st.session_state.active_conv_id ] if visible_convs: st.markdown( "

Conversations

", unsafe_allow_html=True, ) for conv_id, conv in visible_convs[:30]: is_active = conv_id == st.session_state.active_conv_id title = conv.get("title", "New Chat") msg_count = len([m for m in conv.get("messages", []) if m.get("role") == "user"]) col_title, col_menu = st.columns([5, 1]) with col_title: display = f"**{title}**" if is_active else title if st.button( display, key=f"conv_{conv_id}", use_container_width=True, disabled=is_active, ): st.session_state.active_conv_id = conv_id st.session_state.uploaded_context = None st.session_state.upload_key += 1 st.rerun() with col_menu: with st.popover("⋮", use_container_width=True): if st.button("🗑️ Delete", key=f"del_{conv_id}", use_container_width=True): delete_conversation(conv_id) st.rerun() if msg_count > 0: if st.button("📥 Export", key=f"exp_{conv_id}", use_container_width=True): export_data = [ {"role": m.get("role", ""), "content": m.get("content", ""), "emotion": m.get("emotion", ""), "intent": m.get("intent", "")} for m in conv.get("messages", []) ] csv_data = pd.DataFrame(export_data).to_csv(index=False) st.download_button( "Download CSV", csv_data, f"raven_{title[:20]}.csv", "text/csv", key=f"dl_{conv_id}", ) if st.button("📋 Summarize", key=f"sum_{conv_id}", use_container_width=True): emotion_trend = [e for _, e in conv.get("emotion_log", [])] summary_prompt = ( f"Summarise this conversation in 3-4 sentences. " f"Detected emotions: {', '.join(emotion_trend) if emotion_trend else 'none'}. " f"Base your summary strictly on these emotions." ) clean_msgs = [ {"role": m["role"], "content": m["content"]} for m in conv.get("messages", []) if m.get("role") in ["user", "assistant"] and m.get("content") ] summary = get_response( summary_prompt, conv.get("current_emotion", "neutral"), clean_msgs, ) st.info(summary) st.divider() # ── iii) Current Mood & Intent ─────────────────────────────────────────── conv = get_conv() emotion = conv.get("current_emotion", "neutral") style = EMOTION_STYLE.get(emotion, EMOTION_STYLE["neutral"]) color, emoji = style["color"], style["emoji"] st.markdown( "

Mood & Intent

", unsafe_allow_html=True, ) st.markdown( f"
" f"{emoji} {emotion.capitalize()}
", unsafe_allow_html=True, ) intent = conv.get("current_intent", "casual") intent_info = INTENT_STYLE.get(intent, INTENT_STYLE["casual"]) st.markdown( f"
" f"{intent_info['emoji']} {intent_info['label']}
", unsafe_allow_html=True, ) # ── iv) Preferences ────────────────────────────────────────────────────── st.divider() st.markdown( "

Preferences

", unsafe_allow_html=True, ) prefs = st.session_state.preferences new_name = st.text_input( "Your name", value=prefs.get("name", ""), placeholder="So Raven can address you", key="pref_name", label_visibility="collapsed", ) prefs["name"] = new_name new_tone = st.selectbox( "Tone", ["Balanced", "Formal", "Casual", "Concise", "Detailed"], index=["balanced", "formal", "casual", "concise", "detailed"].index( prefs.get("tone", "balanced") ) if prefs.get("tone", "balanced") in ["balanced", "formal", "casual", "concise", "detailed"] else 0, key="pref_tone", label_visibility="collapsed", ) prefs["tone"] = new_tone.lower() # ── v) Emotion Trend Graph ─────────────────────────────────────────────── emotion_log = conv.get("emotion_log", []) if len(emotion_log) >= 2: st.divider() st.markdown( "

Emotion Trend

", unsafe_allow_html=True, ) fig = build_emotion_chart(emotion_log) if fig: st.pyplot(fig, use_container_width=True) plt.close(fig) # ═══════════════════════════════════════════════════════════════════════════════ # MAIN CHAT AREA # ═══════════════════════════════════════════════════════════════════════════════ conv = get_conv() messages = conv.get("messages", []) # ── Welcome screen (when no messages) ──────────────────────────────────────── if not messages: st.markdown("") st.markdown( "
" "

🐦‍⬛

" "

Hello, I'm Raven

" "

" "Your emotionally aware AI assistant — I can code, explain, create, analyze, and more.

" "
", unsafe_allow_html=True, ) cols = st.columns(3) for idx, prompt_item in enumerate(WELCOME_PROMPTS): with cols[idx % 3]: if st.button( f"{prompt_item['emoji']} {prompt_item['title']}\n{prompt_item['prompt'][:50]}...", key=f"welcome_{idx}", use_container_width=True, ): st.session_state.pending_prompt = prompt_item["prompt"] st.rerun() st.markdown( "
" "

" "📄 Upload docs  •  " "📷 Analyze images  •  " "🎙️ Voice input  •  " "📊 Data analysis  •  " "🌐 Web search

" "
", unsafe_allow_html=True, ) # ── Render existing messages ───────────────────────────────────────────────── else: for idx, msg in enumerate(messages): role = msg.get("role") content = msg.get("content", "") with st.chat_message(role): if role == "user": render_badges(msg.get("emotion", "neutral"), msg.get("intent", "casual")) # ── Edit mode for this user message ── if st.session_state.edit_msg_idx == idx: edited_text = st.text_area( "Edit your message:", value=content, key=f"edit_area_{idx}", height=100, label_visibility="collapsed", ) ec1, ec2, ec3 = st.columns([1, 1, 6]) with ec1: if st.button("Save & Resend", key=f"edit_save_{idx}", type="primary"): edited_text = edited_text.strip() if edited_text: # Count how many user messages come after this index # so we can trim emotion_log accordingly user_msgs_after = sum( 1 for m in messages[idx:] if m.get("role") == "user" ) emotion_log = conv.get("emotion_log", []) if user_msgs_after > 0 and len(emotion_log) >= user_msgs_after: del emotion_log[-user_msgs_after:] # Remove all messages from this index onward del messages[idx:] # Trigger re-generation by setting pending_prompt st.session_state.pending_prompt = edited_text st.session_state.edit_msg_idx = None save_conversations() st.rerun() with ec2: if st.button("Cancel", key=f"edit_cancel_{idx}"): st.session_state.edit_msg_idx = None st.rerun() else: st.markdown(content) # Show edit and copy buttons on user messages user_btn_cols = st.columns([0.5, 0.5, 8]) with user_btn_cols[0]: if st.button("✏️", key=f"edit_{idx}", help="Edit message"): st.session_state.edit_msg_idx = idx with user_btn_cols[1]: with st.popover("📋", help="Copy message"): st.code(content, language=None) else: st.markdown(content) # Assistant message: small action buttons if role == "assistant": render_word_count(content) feedback = msg.get("feedback") is_last = idx == len(messages) - 1 # Small inline buttons btn_cols = st.columns([0.5, 0.5, 0.5, 0.5, 7] if is_last else [0.5, 0.5, 0.5, 7.5]) with btn_cols[0]: up_label = "👍" if feedback != "up" else "✅" if st.button(up_label, key=f"up_{idx}", help="Good response"): messages[idx]["feedback"] = "up" save_conversations() st.rerun() with btn_cols[1]: dn_label = "👎" if feedback != "down" else "❌" if st.button(dn_label, key=f"dn_{idx}", help="Bad response"): messages[idx]["feedback"] = "down" save_conversations() st.rerun() with btn_cols[2]: with st.popover("📋", help="Copy"): st.code(content, language=None) if is_last: with btn_cols[3]: if st.button("🔄", key=f"rg_{idx}", help="Regenerate"): st.session_state.regenerate_idx = idx st.rerun() # ═══════════════════════════════════════════════════════════════════════════════ # HANDLE REGENERATION # ═══════════════════════════════════════════════════════════════════════════════ if st.session_state.regenerate_idx is not None: regen_idx = st.session_state.regenerate_idx st.session_state.regenerate_idx = None conv = get_conv() messages = conv.get("messages", []) if regen_idx < len(messages) and messages[regen_idx].get("role") == "assistant": user_msg = None user_emotion = "neutral" for i in range(regen_idx - 1, -1, -1): if messages[i].get("role") == "user": user_msg = messages[i]["content"] user_emotion = messages[i].get("emotion", "neutral") break if user_msg: messages.pop(regen_idx) with st.chat_message("assistant"): with st.status("🔄 Regenerating...", expanded=True) as status: st.write("Generating new response...") status.update(label="Streaming...", state="running") clean_msgs = [ {"role": m["role"], "content": m["content"]} for m in messages if m.get("role") in ["user", "assistant"] and m.get("content") ] stream = get_response_stream(user_msg, user_emotion, clean_msgs) response = st.write_stream(stream) messages.append({"role": "assistant", "content": response}) save_conversations() st.rerun() # ═══════════════════════════════════════════════════════════════════════════════ # CHAT INPUT AREA — 📎 (inside left) + Text input + 🎙️ (inside right) # ═══════════════════════════════════════════════════════════════════════════════ # Show attached files above chat input (thumbnails for images, tags for others) if st.session_state.uploaded_context: ctx = st.session_state.uploaded_context attach_display_col, clear_col = st.columns([9, 1]) with attach_display_col: if ctx["type"] == "image" and isinstance(ctx["content"], list): # Show image thumbnails in a row img_cols = st.columns(min(len(ctx["content"]), 5) + 1) for i, img_bytes in enumerate(ctx["content"][:5]): with img_cols[i]: st.image(img_bytes, width=80, caption=ctx["names"][i] if i < len(ctx.get("names", [])) else f"Image {i+1}") elif ctx["type"] == "image": st.image(ctx["content"], width=80, caption=ctx.get("name", "Image")) else: type_emoji = {"audio": "🎙️", "data": "📊", "document": "📄"}.get(ctx["type"], "📎") st.markdown( f"
" f"{type_emoji} {ctx.get('name', 'File')} attached
", unsafe_allow_html=True, ) with clear_col: if st.button("✕", key="clear_upload", help="Remove attachment"): st.session_state.uploaded_context = None st.session_state.upload_key += 1 st.rerun() # File upload popover (appears above chat, left side) attach_col, spacer_col, voice_col = st.columns([1, 8, 1]) with attach_col: with st.popover("📎", use_container_width=True, help="Attach file"): uploaded_files = st.file_uploader( "Upload files", type=["pdf", "txt", "py", "js", "java", "c", "cpp", "csv", "tsv", "json", "md", "html", "css", "png", "jpg", "jpeg", "gif", "webp", "wav", "mp3", "m4a", "ogg", "flac", "webm", "xlsx", "docx"], label_visibility="collapsed", accept_multiple_files=True, key=f"file_upload_{st.session_state.upload_key}", ) if uploaded_files: # Check if all files are images image_files = [f for f in uploaded_files if f.name.lower().endswith((".png", ".jpg", ".jpeg", ".gif", ".webp"))] if len(image_files) == len(uploaded_files) and len(image_files) > 1: # Multiple images — batch them together (max 5) images_bytes = [] names = [] for img_file in image_files[:5]: images_bytes.append(img_file.read()) names.append(img_file.name) for img_file in image_files[:5]: img_file.seek(0) st.image(img_file, caption=img_file.name, width=100) st.session_state.uploaded_context = {"type": "image", "content": images_bytes, "names": names} st.success(f"📷 {len(images_bytes)} images ready — ask about them!") if len(image_files) > 5: st.warning("Max 5 images per request — first 5 selected.") else: # Single file or mixed — process the first file uploaded_file = uploaded_files[0] content, file_type = process_uploaded_file(uploaded_file) if file_type == "image": st.image(uploaded_file, caption=uploaded_file.name, use_container_width=True) st.session_state.uploaded_context = {"type": "image", "content": content, "name": uploaded_file.name} st.success("📷 Image ready — ask about it!") elif file_type == "audio": st.audio(uploaded_file) with st.spinner("Transcribing..."): transcript = transcribe_audio(content, uploaded_file.name) st.session_state.uploaded_context = {"type": "audio", "content": transcript, "name": uploaded_file.name} st.success("🎙️ Transcribed!") elif file_type == "data": st.session_state.uploaded_context = {"type": "data", "content": content, "name": uploaded_file.name} st.success("📊 Data loaded!") elif file_type == "document": st.session_state.uploaded_context = {"type": "document", "content": content, "name": uploaded_file.name} st.success("📄 Document ready!") doc_action = st.selectbox( "Quick action:", ["Chat about it", "Summarize", "Key Points", "Generate Q&A", "Flashcards", "Simplify", "Study Notes", "Topics"], key="doc_action_chat", ) action_map = { "Summarize": "summarize", "Key Points": "key_points", "Generate Q&A": "questions", "Flashcards": "flashcards", "Simplify": "simplify", "Study Notes": "notes", "Topics": "topics", } if doc_action != "Chat about it" and st.button("🚀 Process"): with st.spinner(f"Processing: {doc_action}..."): result = process_document(content, uploaded_file.name, action_map[doc_action]) st.markdown(result) elif file_type == "error": st.error(content) with voice_col: with st.popover("🎙️", use_container_width=True, help="Voice input"): st.markdown("**🎙️ Record your message:**") audio_input = st.audio_input("Record", key="voice_chat", label_visibility="collapsed") if audio_input: # Only transcribe if we haven't already processed this audio audio_bytes = audio_input.read() audio_hash = hash(audio_bytes[:100]) # quick fingerprint if st.session_state.get("last_audio_hash") != audio_hash: st.session_state.last_audio_hash = audio_hash with st.spinner("Transcribing..."): transcript = transcribe_audio(audio_bytes, "recording.wav") if transcript and not transcript.startswith("Couldn't"): st.session_state.voice_transcript = transcript else: st.error(transcript) # Show editable transcript if available if st.session_state.get("voice_transcript"): st.markdown("---") st.markdown("**✏️ Edit before sending:**") edited = st.text_area( "Edit transcript", value=st.session_state.voice_transcript, key="voice_edit", height=150, label_visibility="collapsed", ) if st.button("📨 Send Message", key="voice_send", type="primary", use_container_width=True): st.session_state.pending_prompt = edited st.session_state.voice_transcript = None st.rerun() # Process pending voice/welcome prompt BEFORE chat_input renders voice_pending = st.session_state.pending_prompt # The actual chat input user_input = st.chat_input("Talk to Raven — ask anything...") # Voice/welcome prompt takes priority if no typed input if not user_input and voice_pending: user_input = voice_pending st.session_state.pending_prompt = None elif user_input: # User typed something — clear any pending prompt st.session_state.pending_prompt = None if user_input: user_input = user_input.strip() if not user_input: st.stop() if len(user_input) > 5000: st.warning("Message too long — keep under 5000 characters.") st.stop() conv = get_conv() messages = conv.get("messages", []) recent_context = get_recent_context() # ── Build extra context ────────────────────────────────────────────────── extra_context = "" prefs = st.session_state.preferences pref_parts = [] if prefs.get("name"): pref_parts.append(f"The user's name is {prefs['name']}. Address them by name occasionally.") if prefs.get("tone") and prefs["tone"] != "balanced": pref_parts.append(f"The user prefers a {prefs['tone']} response style.") if pref_parts: extra_context = "User preferences: " + " ".join(pref_parts) upload_ctx = st.session_state.uploaded_context if upload_ctx: if upload_ctx["type"] == "image": # Image(s) — use vision model directly with st.chat_message("user"): st.markdown(user_input) # Show thumbnail previews of attached images if isinstance(upload_ctx["content"], list): img_cols = st.columns(min(len(upload_ctx["content"]), 5)) for i, img_b in enumerate(upload_ctx["content"][:5]): with img_cols[i]: st.image(img_b, width=80) conv["messages"].append({"role": "user", "content": user_input, "emotion": "neutral", "intent": "question"}) with st.chat_message("assistant"): img_count = len(upload_ctx["content"]) if isinstance(upload_ctx["content"], list) else 1 with st.status("🐦‍⬛ Raven is thinking...", expanded=True) as status: st.write(f"🎨 Analyzing {img_count} image{'s' if img_count > 1 else ''}...") response = analyze_image(upload_ctx["content"], user_input) status.update(label="Done!", state="complete") st.markdown(response) render_word_count(response) conv["messages"].append({"role": "assistant", "content": response}) st.session_state.uploaded_context = None st.session_state.upload_key += 1 if conv.get("title") == "New Chat": conv["title"] = generate_title(user_input) save_conversations() st.rerun() elif upload_ctx["type"] == "audio": extra_context += f"\n\n[Transcribed audio from {upload_ctx['name']}]:\n{upload_ctx['content'][:8000]}" elif upload_ctx["type"] == "data": extra_context += f"\n\n[Data from {upload_ctx['name']}]:\n{upload_ctx['content'][:8000]}" elif upload_ctx["type"] == "document": extra_context += f"\n\n[Document: {upload_ctx['name']}]:\n{upload_ctx['content'][:8000]}" # ── Web search ─────────────────────────────────────────────────────────── searched_web = False try: from web_search import should_search, search_web preliminary_intent = detect_intent(user_input, "neutral", recent_context) if should_search(user_input, preliminary_intent): web_context, _ = search_web(user_input) if web_context: extra_context += f"\n\n{web_context}" searched_web = True except ImportError: pass # ── Emotion & intent ───────────────────────────────────────────────────── primary_emotion, _ = detect_emotion(user_input, recent_context, conv.get("emotion_log", [])) intent = detect_intent(user_input, primary_emotion, recent_context) # Crisis continuation: only if the previous user message had actual crisis keywords direct_crisis = detect_crisis(user_input) prev_user_msgs = [m for m in messages if m.get("role") == "user"] recent_crisis = ( len(prev_user_msgs) > 0 and prev_user_msgs[-1].get("intent") == "crisis" and detect_crisis(prev_user_msgs[-1].get("content", "")) ) if direct_crisis or recent_crisis: primary_emotion = "sad" intent = "crisis" conv["current_emotion"] = primary_emotion conv["current_intent"] = intent conv["messages"].append({ "role": "user", "content": user_input, "emotion": primary_emotion, "intent": intent, }) conv["emotion_log"].append((user_input, primary_emotion)) # ── Display user message ───────────────────────────────────────────────── with st.chat_message("user"): render_badges(primary_emotion, intent) st.markdown(user_input) # ── Stream response ────────────────────────────────────────────────────── with st.chat_message("assistant"): if intent == "crisis" and direct_crisis: # First crisis detection (keywords found) — show helplines directly response = ("I can hear that you're going through something really difficult right now.\n" "You don't have to face this alone. Please reach out to someone who can help:\n\n" "🆘 **iCall (India):** 9152987821\n" "🆘 **Vandrevala Foundation:** 1860-2662-345 (24/7)\n" "🆘 **AASRA:** 9820466726\n" "🆘 **International:** https://www.iasp.info/resources/Crisis_Centres/\n\n" "You matter. I'm here to talk if you need me. 💙") st.markdown(response) elif intent == "crisis" and recent_crisis: # Crisis continuation — send to Groq with gentle crisis-aware prompt extra_context += ("\n\n[IMPORTANT CONTEXT: The user recently expressed crisis-level distress. " "Helpline numbers have ALREADY been provided. Do NOT repeat helpline numbers. " "Instead, be deeply empathetic, warm, and supportive. Acknowledge their feelings, " "gently encourage them, and remind them they are not alone. Do NOT say 'I cannot assist with self-harm' " "or any cold safety refusal. Be a caring friend.]") with st.status("🐦‍⬛ Raven is thinking...", expanded=True) as status: st.write(f"🧠 Emotion: **{primary_emotion}**") st.write(f"🎯 Intent: **{intent}**") status.update(label="Streaming response...", state="running") try: clean_msgs = [ {"role": m["role"], "content": m["content"]} for m in conv["messages"][:-1] if m.get("role") in ["user", "assistant"] and m.get("content") ] stream = get_response_stream(user_input, primary_emotion, clean_msgs, extra_context) response = st.write_stream(stream) except Exception: response = ("I hear you, and I want you to know that your feelings are valid. " "You don't have to go through this alone. I'm right here with you. 💙") st.markdown(response) else: with st.status("🐦‍⬛ Raven is thinking...", expanded=True) as status: st.write(f"🧠 Emotion: **{primary_emotion}**") st.write(f"🎯 Intent: **{intent}**") if searched_web: st.write("🌐 Searched the web...") if upload_ctx: st.write(f"📎 Using: {upload_ctx.get('name', 'file')}") status.update(label="Streaming response...", state="running") try: clean_msgs = [ {"role": m["role"], "content": m["content"]} for m in conv["messages"][:-1] if m.get("role") in ["user", "assistant"] and m.get("content") ] stream = get_response_stream(user_input, primary_emotion, clean_msgs, extra_context) response = st.write_stream(stream) except Exception: response = "I ran into a small issue. Could you try again?" st.markdown(response) render_word_count(response) conv["messages"].append({"role": "assistant", "content": response}) # Clear attachment after it's been used in a message if st.session_state.uploaded_context: st.session_state.uploaded_context = None st.session_state.upload_key += 1 # Auto-title if conv.get("title") == "New Chat" and len([m for m in conv["messages"] if m["role"] == "user"]) == 1: conv["title"] = generate_title(user_input) save_conversations() st.rerun()