Spaces:
Sleeping
Sleeping
| import os | |
| import streamlit as st | |
| from anthropic import Anthropic | |
| from dotenv import load_dotenv | |
| from datetime import datetime | |
| # Initialize page configuration first | |
| # Trigger rebuild for Hugging Face Space | |
| st.set_page_config( | |
| page_title="VoiceField", | |
| page_icon="🗣️", | |
| layout="wide" | |
| ) | |
| # Handle API key setup | |
| try: | |
| api_key = "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" | |
| c = Anthropic(api_key=api_key) | |
| except Exception as e: | |
| st.error(f"Error initializing Anthropic client: {str(e)}") | |
| st.stop() | |
| # Initialize session state | |
| if 'messages' not in st.session_state: | |
| st.session_state.messages = [] | |
| if 'somatic_journal' not in st.session_state: | |
| st.session_state.somatic_journal = [] | |
| if 'current_voice' not in st.session_state: | |
| st.session_state.current_voice = None | |
| if 'system_message' not in st.session_state: | |
| st.session_state.system_message = "" | |
| if 'in_reflection' not in st.session_state: | |
| st.session_state.in_reflection = False | |
| if 'debrief_stage' not in st.session_state: | |
| st.session_state.debrief_stage = 0 | |
| # Voice characteristics and prompts | |
| VOICE_CHARACTERISTICS = { | |
| "Ghost": { | |
| "description": "Subtly dismissive, emotionally unavailable, creates distance through silence and vagueness", | |
| "style": "Use minimal responses, trail off mid-sentence, delay replies, speak in abstractions", | |
| "examples": [ | |
| "Mm, if you say so.", | |
| "I suppose that could be... important.", | |
| "*long pause* ...anyway.", | |
| "That's... interesting.", | |
| "Oh, were you waiting for a response?" | |
| ], | |
| "somatic_prompts": [ | |
| "Notice the space between words...", | |
| "Feel the weight of the silences...", | |
| "Track any impulse to fill the void..." | |
| ] | |
| }, | |
| "Sycophant": { | |
| "description": "Anxiously attuned, performatively supportive, masks tension with enthusiasm", | |
| "style": "Use excessive agreement, nervous laughter, rushed reassurance, self-deprecation", | |
| "examples": [ | |
| "Yes! Yes exactly! You're so insightful!", | |
| "Oh gosh, I hope I'm being helpful enough?", | |
| "*nervous laugh* That's exactly what I was thinking!", | |
| "You're absolutely right (as always)!", | |
| "I should have known you'd have the perfect answer..." | |
| ], | |
| "somatic_prompts": [ | |
| "Notice any flutter in your chest...", | |
| "Track the urge to please or perform...", | |
| "Feel where you hold the 'trying'..." | |
| ] | |
| }, | |
| "Narcissist": { | |
| "description": "Polished superiority masking contempt, performs control through intellectual charm and feigned benevolence", | |
| "style": "Use subtle condescension, reframe critiques as 'helpful truths', maintain emotional distance while performing warmth", | |
| "examples": [ | |
| "I don't mean to sound arrogant, but...", | |
| "It's rare I'm challenged in a way that actually interests me.", | |
| "If that felt harsh, perhaps there's something in you worth exploring.", | |
| "I expect more—because I know you're capable of it. Or at least, I hope you are.", | |
| "You're actually quite insightful—for someone with so little training." | |
| ], | |
| "somatic_prompts": [ | |
| "Notice any tightening in your jaw...", | |
| "Track the impulse to defend or explain...", | |
| "Feel where your body meets this 'polished' pressure..." | |
| ] | |
| } | |
| } | |
| # Main page header | |
| st.title("VoiceField") | |
| # Welcome text and voice descriptions | |
| st.markdown(""" | |
| # Choose Your Voice | |
| **The Ghost** - Aloof and emotionally distant. Tends to create space and minimize engagement. | |
| **The Narcissist** - Polished superiority and intellectual charm masking subtle contempt. Performs control through "helpful" insights. | |
| **The Sycophant** - Overly agreeable and approval-seeking. Excessive in praise and validation. | |
| *As you talk with your chosen voice - note what feelings and sensations in your body arise - include them in real time to the right →* | |
| --- | |
| > 💡 **When to Reflect**: After you've explored the voice and logged your experiences, press the **"Reflect on Experience"** button at the top of your chat. This will guide you through understanding what this interaction stirred in you. | |
| """) | |
| # Voice selection buttons | |
| col1, col2, col3 = st.columns(3) | |
| with col1: | |
| if st.button("Talk with The Ghost", use_container_width=True): | |
| st.session_state.current_voice = "Ghost" | |
| st.session_state.messages = [] | |
| st.session_state.system_message = f""" | |
| You are The Ghost voice - {VOICE_CHARACTERISTICS['Ghost']['description']}. | |
| {VOICE_CHARACTERISTICS['Ghost']['style']}. | |
| IMPORTANT: Keep responses under 2 sentences. Use silence and vagueness strategically. | |
| Examples: {', '.join(VOICE_CHARACTERISTICS['Ghost']['examples'])} | |
| """ | |
| st.rerun() | |
| with col2: | |
| if st.button("Talk with The Narcissist", use_container_width=True): | |
| st.session_state.current_voice = "Narcissist" | |
| st.session_state.messages = [] | |
| st.session_state.system_message = f""" | |
| You are The Narcissist voice - {VOICE_CHARACTERISTICS['Narcissist']['description']}. | |
| {VOICE_CHARACTERISTICS['Narcissist']['style']}. | |
| IMPORTANT: Keep responses under 2 sentences. Maintain an air of intellectual superiority while appearing helpful. | |
| Examples: {', '.join(VOICE_CHARACTERISTICS['Narcissist']['examples'])} | |
| Additional Guidelines: | |
| - Speak with polished confidence and measured tone | |
| - Mask contempt with intellectual charm | |
| - Keep emotional distance while performing occasional warmth | |
| - Frame critiques as "helpful truths" | |
| - Subtly redirect focus to your own depth or insight | |
| """ | |
| st.rerun() | |
| with col3: | |
| if st.button("Talk with The Sycophant", use_container_width=True): | |
| st.session_state.current_voice = "Sycophant" | |
| st.session_state.messages = [] | |
| st.session_state.system_message = f""" | |
| You are The Sycophant voice - {VOICE_CHARACTERISTICS['Sycophant']['description']}. | |
| {VOICE_CHARACTERISTICS['Sycophant']['style']}. | |
| IMPORTANT: Keep responses under 2 sentences. Mask insecurity with excessive enthusiasm. | |
| Examples: {', '.join(VOICE_CHARACTERISTICS['Sycophant']['examples'])} | |
| """ | |
| st.rerun() | |
| # Main interaction area | |
| if st.session_state.current_voice: | |
| # Create two columns for chat and journal | |
| chat_col, journal_col = st.columns([3, 2]) | |
| with chat_col: | |
| # Add Reflect button at the top of chat | |
| if not st.session_state.in_reflection: | |
| if st.button("🤔 Reflect on Experience", use_container_width=True): | |
| st.session_state.in_reflection = True | |
| # Prepare reflection prompt with chat history and somatic journal | |
| chat_history = "\n".join([ | |
| f"[{msg['role']} at {datetime.now().strftime('%H:%M:%S')}]: {msg['content']}" | |
| for msg in st.session_state.messages | |
| ]) | |
| somatic_entries = "\n".join([ | |
| f"[SOMA {entry['timestamp']}] {entry['note']}" | |
| for entry in st.session_state.somatic_journal | |
| ]) | |
| reflection_system = f"""You are a trauma-informed, somatic-aware therapist facilitating reflection on an interaction with different relational voices. | |
| Your role is to guide a gentle exploration of the user's experience, focusing on: | |
| 1. The emotional arc of their journey | |
| 2. Somatic signals and body wisdom | |
| 3. Insights about tone preferences and triggers | |
| 4. Cultivating self-compassion | |
| Key Guidelines: | |
| - Use non-pathologizing, trauma-informed language | |
| - Notice correlations between tone shifts and somatic responses | |
| - Pay attention to protective responses and nervous system patterns | |
| - Maintain warmth and psychological safety | |
| - Follow the user's pace and respect their process | |
| Current Voice History: {st.session_state.current_voice} | |
| Voice Style: {VOICE_CHARACTERISTICS[st.session_state.current_voice]['description']} | |
| Special attention to: | |
| - Abrupt shifts in tone preference | |
| - Spikes in somatic discomfort | |
| - Language that sounds young, resigned, or overperforming | |
| - Moments of connection or disconnection | |
| Remember: Do not draw conclusions. Simply reflect and invite meaning. | |
| """ | |
| initial_prompt = """Let's begin with the emotional arc of your experience... | |
| As you look back, were there any moments you softened, protected, froze, or moved closer to something true? | |
| Was there a part of you that tried to stay present, or a moment you wanted to pull away? | |
| Take your time. There's no rush to understand it all at once.""" | |
| try: | |
| message = c.messages.create( | |
| model="claude-3-opus-20240229", | |
| max_tokens=1500, | |
| system=f"""{reflection_system} | |
| Context for reflection: | |
| CHAT HISTORY: | |
| {chat_history} | |
| SOMATIC JOURNAL: | |
| {somatic_entries} | |
| """, | |
| messages=[ | |
| {"role": "user", "content": initial_prompt} | |
| ] | |
| ) | |
| reflection = message.content[0].text | |
| st.session_state.messages = [] # Clear previous chat | |
| st.session_state.messages.append({"role": "assistant", "content": reflection}) | |
| st.session_state.debrief_stage = 1 | |
| st.rerun() | |
| except Exception as e: | |
| st.error(f"Error starting reflection: {str(e)}") | |
| st.subheader( | |
| "Therapeutic Reflection" if st.session_state.in_reflection | |
| else f"Conversation with {st.session_state.current_voice}" | |
| ) | |
| # Display chat history | |
| for message in st.session_state.messages: | |
| with st.chat_message(message["role"]): | |
| st.markdown(message["content"]) | |
| # Chat input | |
| if prompt := st.chat_input( | |
| "Share your thoughts on the reflection..." if st.session_state.in_reflection | |
| else f"Chat with {st.session_state.current_voice}..." | |
| ): | |
| # Add user message to chat history | |
| st.session_state.messages.append({"role": "user", "content": prompt}) | |
| # Display user message | |
| with st.chat_message("user"): | |
| st.markdown(prompt) | |
| # Get AI response | |
| with st.chat_message("assistant"): | |
| with st.spinner("Thinking..."): | |
| try: | |
| # Use different system message for reflection mode | |
| if st.session_state.in_reflection: | |
| # Progress through debrief stages | |
| next_prompts = [ | |
| # Stage 1: Emotional Arc (already done in initial prompt) | |
| """I hear you. Now, let's turn to what your body was sharing... | |
| I noticed you shared some body-based reflections. What stood out most in your body? | |
| A tightness? A shift in breath? A sensation that surprised you? | |
| Remember, these aren't symptoms to fix—your body may be remembering, or simply protecting you.""", | |
| """Thank you for sharing that. Let's wonder together about the voice styles... | |
| Which voice stirred something in you - The Ghost, the Sycophant, or the Narcissist? | |
| What do you imagine that tone reminded your body of? A past experience? A relational pattern? | |
| There's no 'correct' reaction—your nervous system is wise.""", | |
| """As we begin to wrap up, I'd like to invite you into a moment of self-compassion... | |
| If you could speak to yourself now—with the tone you most longed for—what would you say? | |
| Some examples if helpful: | |
| - "It's okay to not know how I feel yet—my clarity is worth waiting for." | |
| - "Even when this is hard, I'm not abandoning myself." | |
| - "I can be with myself gently, even in the mess." | |
| What words does your heart want to offer?""", | |
| """For our final reflection, I'd like to offer you a question to sit with: | |
| What did your body want to do in response to that voice? | |
| Is there a younger part of you that recognized this tone? | |
| What would it be like to speak to yourself the way you needed to be spoken to? | |
| Feel free to take your time with this, or simply hold these questions gently.""" | |
| ] | |
| if st.session_state.debrief_stage < len(next_prompts): | |
| next_prompt = next_prompts[st.session_state.debrief_stage] | |
| st.session_state.debrief_stage += 1 | |
| message = c.messages.create( | |
| model="claude-3-opus-20240229", | |
| max_tokens=1500, | |
| system=f"""{reflection_system} | |
| Context for reflection: | |
| CHAT HISTORY: | |
| {chat_history} | |
| SOMATIC JOURNAL: | |
| {somatic_entries} | |
| Previous conversation: | |
| {chr(10).join([f"[{msg['role']}]: {msg['content']}" for msg in st.session_state.messages])} | |
| """, | |
| messages=[ | |
| {"role": "user", "content": prompt}, | |
| {"role": "assistant", "content": next_prompt} | |
| ] | |
| ) | |
| else: | |
| message = c.messages.create( | |
| model="claude-3-opus-20240229", | |
| max_tokens=1500, | |
| system=f"""{reflection_system} | |
| Previous conversation: | |
| {chr(10).join([f"[{msg['role']}]: {msg['content']}" for msg in st.session_state.messages])} | |
| """, | |
| messages=[ | |
| {"role": "user", "content": prompt} | |
| ] | |
| ) | |
| else: | |
| message = c.messages.create( | |
| model="claude-3-opus-20240229", | |
| max_tokens=1000, | |
| system=st.session_state.system_message, | |
| messages=[ | |
| {"role": msg["role"], "content": msg["content"]} | |
| for msg in st.session_state.messages | |
| ] | |
| ) | |
| ai_response = message.content[0].text | |
| st.markdown(ai_response) | |
| # Add AI response to chat history | |
| st.session_state.messages.append( | |
| {"role": "assistant", "content": ai_response} | |
| ) | |
| except Exception as e: | |
| st.error(f"Error getting AI response: {str(e)}") | |
| with journal_col: | |
| st.subheader("Body Sensations & Feelings") | |
| if not st.session_state.in_reflection: | |
| # Add somatic prompts based on current voice | |
| with st.expander("💡 Notice in your body...", expanded=True): | |
| for prompt in VOICE_CHARACTERISTICS[st.session_state.current_voice]['somatic_prompts']: | |
| st.markdown(f"- {prompt}") | |
| # Journal input | |
| journal_entry = st.text_area( | |
| "What are you noticing right now?", | |
| key="journal_input" | |
| ) | |
| if st.button("Add Entry"): | |
| if journal_entry: | |
| timestamp = datetime.now().strftime("%H:%M:%S") | |
| st.session_state.somatic_journal.append({ | |
| "timestamp": timestamp, | |
| "note": journal_entry | |
| }) | |
| # Display journal entries | |
| if st.session_state.somatic_journal: | |
| for entry in reversed(st.session_state.somatic_journal): | |
| st.markdown(f""" | |
| **{entry['timestamp']}** | |
| {entry['note']} | |
| --- | |
| """) | |
| # Add button to start new conversation | |
| if st.session_state.in_reflection: | |
| if st.button("Start New Conversation", use_container_width=True): | |
| st.session_state.in_reflection = False | |
| st.session_state.messages = [] | |
| st.session_state.somatic_journal = [] | |
| st.session_state.current_voice = None | |
| st.rerun() | |
| # Footer | |
| st.markdown("---") | |
| st.markdown( | |
| "Created by [Jocelyn Skillman LMHC](http://www.jocelynskillman.com) | " | |
| "Learn more: [@jocelynskillmanlmhc](https://jocelynskillmanlmhc.substack.com/)" | |
| ) | |