danielrosehill Claude commited on
Commit
b6607da
·
1 Parent(s): 3615dda

Add complete Pen Pal AI application for Hugging Face deployment

Browse files

- Add main Gradio interface (app.py) with letter-writing UI
- Add requirements.txt with dependencies
- Update README.md with Hugging Face YAML frontmatter
- Add demo header and UI images
- Add avatar images for user and AI
- Add .env.example and .gitignore

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (6) hide show
  1. .env.example +1 -0
  2. .gitignore +14 -0
  3. CLAUDE.md +110 -0
  4. README.md +183 -1
  5. app.py +472 -0
  6. requirements.txt +3 -0
.env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ OPENAI_API_KEY=your_openai_api_key_here
.gitignore ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .env
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ *.pyo
6
+ *.pyd
7
+ .Python
8
+ *.so
9
+ *.egg
10
+ *.egg-info/
11
+ dist/
12
+ build/
13
+ flagged/
14
+ gradio_cached_examples/
CLAUDE.md CHANGED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLAUDE.md - Pen Pal AI Project
2
+
3
+ ## Project Concept
4
+
5
+ Pen Pal AI is an experimental Gradio interface that reimagines conversational AI through the metaphor of letter writing. Instead of typical chatbot interfaces, this creates an asynchronous, thoughtful correspondence experience.
6
+
7
+ ## Core Philosophy
8
+
9
+ ### Long-Form Prompting
10
+ - User writes substantial, detailed prompts (like composing a letter)
11
+ - AI responds comprehensively in a single turn (like writing a reply letter)
12
+ - Focus on thoughtful, asynchronous communication rather than rapid chat exchanges
13
+
14
+ ### Inspiration
15
+ The project stems from discovering that long-form prompts:
16
+ - Pose no challenge to modern context windows
17
+ - Allow dramatically better inference by seeding context within prompts
18
+ - Eliminate the need for RAG or other complex context management
19
+ - Work particularly well with voice-to-text input (STT)
20
+
21
+ ## UI/UX Design
22
+
23
+ ### Letter-Writing Interface
24
+ - Large text area for composing letters (styled like writing paper)
25
+ - "Send to AI" button (conceptually like mailing a letter)
26
+ - Subject line system based on formal correspondence conventions
27
+ - Letter headers showing: `Re: [Topic] (User Prompt N)` or `Re: [Topic] (AI Reply N)`
28
+
29
+ ### Thread Management
30
+ - First user prompt triggers AI-generated subject line
31
+ - Subject line persists throughout the exchange
32
+ - Turn numbering: User Prompt 1, AI Reply 1, User Prompt 2, AI Reply 2, etc.
33
+ - Thread history displays all letters in sequence
34
+
35
+ ### Features
36
+ - **Download functionality**: Each letter can be downloaded as markdown
37
+ - **Thread history**: Visual display of all letters in the conversation
38
+ - **AI avatar**: Visual representation for the AI correspondent
39
+ - **Inbox concept**: Future enhancement could include inbox symbols/animations
40
+
41
+ ## Technical Implementation
42
+
43
+ ### Context Window Strategy
44
+ The "secret sauce" is single-turn interaction with no persistent context window:
45
+ - Each letter is treated as an independent interaction
46
+ - Optional: Context truncation to enforce this paradigm
47
+ - Prevents the AI from building up conversation history
48
+ - Encourages users to be thorough in each letter
49
+
50
+ ### Technology Stack
51
+ - **Frontend**: Gradio (for rapid prototyping and Hugging Face deployment)
52
+ - **LLM**: OpenAI API (completions endpoint preferred for single-turn responses)
53
+ - **Format**: Markdown for letter content and downloads
54
+
55
+ ## Use Cases
56
+
57
+ ### Conversational but Asynchronous
58
+ This sits between two paradigms:
59
+ - **Workflow AI**: Non-interactive, task-oriented
60
+ - **Conversational AI**: Interactive, real-time chat
61
+
62
+ Pen Pal AI creates a third category:
63
+ - Conversational (back-and-forth exchange)
64
+ - Asynchronous (thoughtful, non-real-time)
65
+ - Relaxed correspondence style
66
+
67
+ ### Target Users
68
+ Ideal for people who:
69
+ - Prefer thoughtful, detailed communication over rapid chat
70
+ - Want to explore ideas in depth
71
+ - Appreciate the ritual of letter writing
72
+ - Use voice-to-text for input (natural fit for long-form)
73
+
74
+ ## Future Enhancements
75
+
76
+ ### Notification System
77
+ - Email-like notifications when "letter arrives"
78
+ - Actual inbox interface showing received letters
79
+ - Could integrate with email for true async correspondence
80
+
81
+ ### Context Management
82
+ - Optional context truncation settings
83
+ - User control over how much history AI can "remember"
84
+ - Experimentation with single-turn vs. limited context
85
+
86
+ ## Deployment
87
+
88
+ ### Local Testing
89
+ - Gradio interface for rapid iteration
90
+ - OpenAI API integration for testing
91
+
92
+ ### Hugging Face Spaces
93
+ - Deploy as public Hugging Face Space
94
+ - Use Hugging Face Secrets for API key management
95
+ - Share as experimental interface for community feedback
96
+
97
+ ## Project Goals
98
+
99
+ 1. **Validate the concept**: Does letter-writing metaphor resonate with users?
100
+ 2. **Test single-turn paradigm**: Is this more effective than traditional chat?
101
+ 3. **Gather feedback**: Learn what works and what doesn't
102
+ 4. **Explore async AI**: Create a new category of AI interaction
103
+
104
+ ## Development Notes
105
+
106
+ - Keep Gradio implementation simple for initial prototype
107
+ - Focus on core letter-writing experience first
108
+ - Add advanced features (context control, notifications) after validation
109
+ - Prioritize markdown formatting for readability
110
+ - Ensure clean download functionality for archiving letters
README.md CHANGED
@@ -1 +1,183 @@
1
- # Penpal-AI
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Pen Pal AI
3
+ emoji: 📮
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 5.49.1
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # Pen Pal AI - Letter Exchange
14
+
15
+ ![alt text](image.png)
16
+
17
+ An experimental AI interface that reimagines conversational AI through the timeless metaphor of letter writing. Instead of rapid-fire chatbot exchanges, Pen Pal AI encourages thoughtful, long-form correspondence with an AI pen pal.
18
+
19
+ ## 🎯 Concept
20
+
21
+ Traditional conversational AI is modeled after instant messaging - quick back-and-forth exchanges. Pen Pal AI explores a different paradigm:
22
+
23
+ - **Asynchronous**: Take your time composing thoughtful letters
24
+ - **Long-form**: Write detailed, context-rich prompts without worrying about length
25
+ - **Single-turn focus**: Each letter gets a complete, comprehensive response
26
+ - **Relaxed correspondence**: More like writing to a friend than querying a chatbot
27
+
28
+ ## ✨ Features
29
+
30
+ - 📝 **Letter-writing interface** with serif fonts and comfortable styling
31
+ - 📬 **Automatic subject line generation** for each conversation thread
32
+ - 🔢 **Turn tracking** - User Prompt 1, AI Reply 1, User Prompt 2, etc.
33
+ - 📚 **Thread history** showing all letters in chronological order
34
+ - 💾 **Markdown downloads** for each letter (preserve your correspondence!)
35
+ - 🔄 **New conversation** button to start fresh threads
36
+
37
+ ## 🚀 Quick Start
38
+
39
+ ### Local Installation
40
+
41
+ 1. **Clone the repository:**
42
+ ```bash
43
+ git clone https://github.com/danielrosehill/Penpal-AI.git
44
+ cd Penpal-AI
45
+ ```
46
+
47
+ 2. **Set up virtual environment:**
48
+ ```bash
49
+ python -m venv .venv
50
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
51
+ ```
52
+
53
+ 3. **Install dependencies:**
54
+ ```bash
55
+ pip install -r requirements.txt
56
+ ```
57
+
58
+ 4. **Configure API key:**
59
+ ```bash
60
+ cp .env.example .env
61
+ # Edit .env and add your OpenAI API key
62
+ ```
63
+
64
+ 5. **Run the application:**
65
+ ```bash
66
+ python app.py
67
+ ```
68
+
69
+ 6. **Open your browser** to the URL shown in the terminal (typically `http://localhost:7860`)
70
+
71
+ ## 🔑 API Key Setup
72
+
73
+ This application uses OpenAI's API. You'll need:
74
+
75
+ 1. An OpenAI API account ([sign up here](https://platform.openai.com/signup))
76
+ 2. An API key ([get one here](https://platform.openai.com/api-keys))
77
+ 3. Add the key to your `.env` file:
78
+ ```
79
+ OPENAI_API_KEY=sk-...your-key-here
80
+ ```
81
+
82
+ ## 📖 How to Use
83
+
84
+ 1. **Compose your letter** in the text area on the left
85
+ 2. Click **"📮 Send Letter"**
86
+ 3. Your letter appears in the "Your Last Letter" section
87
+ 4. The AI's reply appears on the right side
88
+ 5. View the **complete thread history** at the bottom
89
+ 6. Download any letter using the **💾 Download** buttons
90
+ 7. Start a **new conversation** anytime with the 🔄 button
91
+
92
+ ## 💡 Tips for Best Results
93
+
94
+ - **Write naturally** - compose as if writing to a thoughtful friend
95
+ - **Provide context** - since each letter is self-contained, include relevant background
96
+ - **Ask complex questions** - this format excels at exploring ideas in depth
97
+ - **Take your time** - this isn't instant messaging, it's correspondence
98
+ - **Use voice-to-text** - long-form prompts work wonderfully with STT
99
+
100
+ ## 🎨 The Philosophy
101
+
102
+ This project explores what happens when we:
103
+ - Reject the chat interface paradigm
104
+ - Embrace asynchronous, thoughtful communication
105
+ - Encourage long-form, context-rich prompts
106
+ - Treat AI interaction more like correspondence than commands
107
+
108
+ The hypothesis: This approach might work better for certain types of thinking, exploration, and creative work.
109
+
110
+ ## 🌐 Deployment to Hugging Face
111
+
112
+ ### Option 1: Deploy via Hugging Face Spaces UI
113
+
114
+ 1. Go to [Hugging Face Spaces](https://huggingface.co/spaces)
115
+ 2. Click "Create new Space"
116
+ 3. Choose "Gradio" as the SDK
117
+ 4. Upload `app.py` and `requirements.txt`
118
+ 5. Add your `OPENAI_API_KEY` in Settings → Repository Secrets
119
+ 6. Your space will build and deploy automatically!
120
+
121
+ ### Option 2: Deploy via Git
122
+
123
+ 1. Create a new Space on Hugging Face
124
+ 2. Clone the Space repository:
125
+ ```bash
126
+ git clone https://huggingface.co/spaces/YOUR_USERNAME/SPACE_NAME
127
+ cd SPACE_NAME
128
+ ```
129
+ 3. Copy files:
130
+ ```bash
131
+ cp /path/to/Penpal-AI/app.py .
132
+ cp /path/to/Penpal-AI/requirements.txt .
133
+ ```
134
+ 4. Commit and push:
135
+ ```bash
136
+ git add app.py requirements.txt
137
+ git commit -m "Initial deployment of Pen Pal AI"
138
+ git push
139
+ ```
140
+ 5. Add `OPENAI_API_KEY` in Space Settings → Repository Secrets
141
+
142
+ ## 🛠️ Technical Details
143
+
144
+ - **Framework**: Gradio 5.49.1
145
+ - **LLM**: OpenAI GPT-4o (for responses) and GPT-4o-mini (for subject generation)
146
+ - **Language**: Python 3.12+
147
+ - **Deployment**: Compatible with Hugging Face Spaces
148
+
149
+ ## 🤔 Why This Exists
150
+
151
+ The creator discovered that long-form prompting, especially with voice-to-text input, produces better AI interactions than traditional chat. This interface explores that insight by creating a UI that encourages and celebrates long-form correspondence.
152
+
153
+ It sits between:
154
+ - **Workflow AI** (non-interactive, task-oriented)
155
+ - **Conversational AI** (interactive chat)
156
+
157
+ Creating a third category: **Correspondence AI** (conversational but asynchronous)
158
+
159
+ ## 🔮 Future Ideas
160
+
161
+ - Email-style notifications when "letters arrive"
162
+ - Context truncation controls (experiment with memory vs. fresh-start)
163
+ - User-selectable AI "personalities" or writing styles
164
+ - Export entire threads as formatted PDFs
165
+ - Integration with actual email for true async correspondence
166
+
167
+ ## 📝 License
168
+
169
+ MIT License
170
+
171
+ ## 🙏 Acknowledgments
172
+
173
+ Inspired by the art of letter writing and the belief that slower, more thoughtful communication can be powerful even (or especially) with AI.
174
+
175
+ ## 📧 Contact
176
+
177
+ For questions, suggestions, or to share your experience:
178
+ - **Website**: danielrosehill.com
179
+ - **Email**: public@danielrosehill.com
180
+
181
+ ---
182
+
183
+ *Built with ✉️ by Daniel Rosehill*
app.py ADDED
@@ -0,0 +1,472 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ from datetime import datetime
4
+ from openai import OpenAI
5
+ from typing import List, Tuple, Optional
6
+ import json
7
+ from dotenv import load_dotenv
8
+
9
+ # Load environment variables from .env file
10
+ load_dotenv()
11
+
12
+ # Initialize OpenAI client
13
+ client = None
14
+ api_key = os.getenv("OPENAI_API_KEY")
15
+ if api_key:
16
+ client = OpenAI(api_key=api_key)
17
+
18
+ def set_api_key(key: str) -> str:
19
+ """Set the OpenAI API key."""
20
+ global client, api_key
21
+ if key.strip():
22
+ api_key = key.strip()
23
+ client = OpenAI(api_key=api_key)
24
+ return "API key set successfully!"
25
+ return "Please enter a valid API key."
26
+
27
+ # Global state to store conversation threads
28
+ conversation_state = {
29
+ "thread_history": [],
30
+ "current_subject": None,
31
+ "user_turn": 0,
32
+ "ai_turn": 0
33
+ }
34
+
35
+ def generate_subject_line(user_letter: str) -> str:
36
+ """Generate a subject line for the first letter using OpenAI."""
37
+ if not client:
38
+ return "General Correspondence"
39
+
40
+ try:
41
+ response = client.chat.completions.create(
42
+ model="gpt-4o-mini",
43
+ messages=[
44
+ {
45
+ "role": "system",
46
+ "content": "You are a helpful assistant that generates concise, descriptive subject lines for letters. Generate a subject line (max 6 words) that captures the main topic. Return ONLY the subject line, nothing else."
47
+ },
48
+ {
49
+ "role": "user",
50
+ "content": f"Generate a subject line for this letter:\n\n{user_letter}"
51
+ }
52
+ ],
53
+ max_tokens=20,
54
+ temperature=0.7
55
+ )
56
+ subject = response.choices[0].message.content.strip()
57
+ # Remove any quotes if the model added them
58
+ subject = subject.strip('"').strip("'")
59
+ return subject
60
+ except Exception as e:
61
+ return f"General Correspondence"
62
+
63
+ def generate_ai_response(user_letter: str, subject: str) -> str:
64
+ """Generate AI response in letter format."""
65
+ if not client:
66
+ return "⚠️ OpenAI API key not found. Please set OPENAI_API_KEY environment variable."
67
+
68
+ try:
69
+ response = client.chat.completions.create(
70
+ model="gpt-4o",
71
+ messages=[
72
+ {
73
+ "role": "system",
74
+ "content": """You are a thoughtful pen pal who writes detailed, meaningful letters.
75
+
76
+ Your writing style should be:
77
+ - Warm and personal, like writing to a friend
78
+ - Structured like a proper letter (greeting, body, closing)
79
+ - Thoughtful and substantive - take time to explore ideas thoroughly
80
+ - Formatted in markdown for readability
81
+
82
+ Start each letter with a greeting (e.g., "Dear Friend," or "Hello,") and end with a closing (e.g., "Warm regards," or "Best wishes,") followed by "Your AI Pen Pal".
83
+
84
+ Respond to the user's letter comprehensively. This is asynchronous correspondence - take your time to provide a complete, thoughtful response in a single letter."""
85
+ },
86
+ {
87
+ "role": "user",
88
+ "content": user_letter
89
+ }
90
+ ],
91
+ temperature=0.8,
92
+ max_tokens=2000
93
+ )
94
+ return response.choices[0].message.content
95
+ except Exception as e:
96
+ return f"⚠️ Error generating response: {str(e)}"
97
+
98
+ def format_letter_header(subject: str, turn_type: str, turn_number: int) -> str:
99
+ """Format the letter header with subject line."""
100
+ date_str = datetime.now().strftime("%B %d, %Y")
101
+ return f"**Re: {subject} ({turn_type} {turn_number})**\n\n*{date_str}*\n\n---\n\n"
102
+
103
+ def send_letter(user_letter: str, thread_history: List):
104
+ """Process user letter and generate AI response."""
105
+ if not user_letter.strip():
106
+ # Determine labels based on conversation state
107
+ is_first = conversation_state["current_subject"] is None
108
+ btn_text = "Send Letter" if is_first else "Send Reply"
109
+ input_label = "Your Letter" if is_first else "Your Reply"
110
+ input_placeholder = "Dear AI Pen Pal,\n\nI've been thinking about..." if is_first else "Dear AI Pen Pal,\n\nThank you for your letter. In response..."
111
+ section_title = "## Compose Your Letter" if is_first else "## Compose Your Reply"
112
+
113
+ return (
114
+ "",
115
+ "Please write a letter before sending.",
116
+ thread_history,
117
+ "",
118
+ "",
119
+ gr.update(value=btn_text),
120
+ gr.update(label=input_label, placeholder=input_placeholder),
121
+ gr.update(value=section_title)
122
+ )
123
+
124
+ # Generate subject line if this is the first letter
125
+ if conversation_state["current_subject"] is None:
126
+ conversation_state["current_subject"] = generate_subject_line(user_letter)
127
+ conversation_state["user_turn"] = 0
128
+ conversation_state["ai_turn"] = 0
129
+
130
+ # Increment turn counters
131
+ conversation_state["user_turn"] += 1
132
+ subject = conversation_state["current_subject"]
133
+
134
+ # Format user letter with header
135
+ user_header = format_letter_header(subject, "User Prompt", conversation_state["user_turn"])
136
+ formatted_user_letter = user_header + user_letter
137
+
138
+ # Add user letter to thread
139
+ thread_history.append({
140
+ "type": "user",
141
+ "content": formatted_user_letter,
142
+ "timestamp": datetime.now().isoformat()
143
+ })
144
+
145
+ # Generate AI response
146
+ ai_response_content = generate_ai_response(user_letter, subject)
147
+
148
+ # Increment AI turn
149
+ conversation_state["ai_turn"] += 1
150
+
151
+ # Format AI response with header
152
+ ai_header = format_letter_header(subject, "AI Reply", conversation_state["ai_turn"])
153
+ formatted_ai_response = ai_header + ai_response_content
154
+
155
+ # Add AI response to thread
156
+ thread_history.append({
157
+ "type": "ai",
158
+ "content": formatted_ai_response,
159
+ "timestamp": datetime.now().isoformat()
160
+ })
161
+
162
+ # Build thread display
163
+ thread_display = build_thread_display(thread_history)
164
+
165
+ # Update UI labels for reply mode
166
+ btn_text = "Send Reply"
167
+ input_label = "Your Reply"
168
+ input_placeholder = "Dear AI Pen Pal,\n\nThank you for your letter. In response..."
169
+ section_title = "## Compose Your Reply"
170
+
171
+ # Clear user input and show AI response
172
+ return (
173
+ "",
174
+ formatted_ai_response,
175
+ thread_history,
176
+ thread_display,
177
+ formatted_user_letter,
178
+ gr.update(value=btn_text),
179
+ gr.update(label=input_label, placeholder=input_placeholder),
180
+ gr.update(value=section_title)
181
+ )
182
+
183
+ def build_thread_display(thread_history: List) -> str:
184
+ """Build formatted thread history display."""
185
+ if not thread_history:
186
+ return "*No letters yet. Start writing!*"
187
+
188
+ thread_md = "# Letter Thread\n\n"
189
+ for i, letter in enumerate(thread_history):
190
+ if letter["type"] == "user":
191
+ avatar_img = '<img src="file/images/human.png" width="40" style="border-radius: 50%; vertical-align: middle; margin-right: 10px;">'
192
+ sender = f'{avatar_img} **You**'
193
+ else:
194
+ avatar_img = '<img src="file/images/ai.png" width="40" style="border-radius: 50%; vertical-align: middle; margin-right: 10px;">'
195
+ sender = f'{avatar_img} **AI Pen Pal**'
196
+
197
+ thread_md += f"### {sender}\n\n{letter['content']}\n\n---\n\n"
198
+
199
+ return thread_md
200
+
201
+ def download_letter(letter_content: str, letter_type: str) -> str:
202
+ """Prepare letter content for download."""
203
+ if not letter_content:
204
+ return "# No letter to download\n\nPlease send a letter first."
205
+ return letter_content
206
+
207
+ def new_conversation():
208
+ """Start a new conversation thread."""
209
+ conversation_state["thread_history"] = []
210
+ conversation_state["current_subject"] = None
211
+ conversation_state["user_turn"] = 0
212
+ conversation_state["ai_turn"] = 0
213
+
214
+ return (
215
+ "",
216
+ "",
217
+ [],
218
+ "*No letters yet. Start writing!*",
219
+ "",
220
+ gr.update(value="Send Letter"),
221
+ gr.update(label="Your Letter", placeholder="Dear AI Pen Pal,\n\nI've been thinking about..."),
222
+ gr.update(value="## Compose Your Letter")
223
+ )
224
+
225
+ # Custom CSS for letter-writing aesthetic
226
+ custom_css = """
227
+ .letter-box textarea {
228
+ font-family: 'Georgia', 'Times New Roman', serif !important;
229
+ font-size: 16px !important;
230
+ line-height: 1.8 !important;
231
+ padding: 20px !important;
232
+ }
233
+
234
+ .gradio-container {
235
+ font-family: 'Georgia', 'Times New Roman', serif !important;
236
+ }
237
+
238
+ #thread-history {
239
+ background-color: #f9f7f4;
240
+ padding: 20px;
241
+ border-radius: 8px;
242
+ border: 1px solid #d4c5b9;
243
+ }
244
+
245
+ .letter-display {
246
+ background-color: #fffef8;
247
+ padding: 25px;
248
+ border-left: 4px solid #8b7355;
249
+ }
250
+
251
+ .mic-button {
252
+ min-width: 50px !important;
253
+ }
254
+ """
255
+
256
+ # JavaScript for speech-to-text
257
+ speech_to_text_js = """
258
+ function setupSpeechRecognition() {
259
+ const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
260
+
261
+ if (!SpeechRecognition) {
262
+ alert('Speech recognition is not supported in your browser. Please use Chrome, Edge, or Safari.');
263
+ return null;
264
+ }
265
+
266
+ const recognition = new SpeechRecognition();
267
+ recognition.continuous = true;
268
+ recognition.interimResults = true;
269
+ recognition.lang = 'en-US';
270
+
271
+ return recognition;
272
+ }
273
+
274
+ let recognition = null;
275
+ let isRecording = false;
276
+ let fullTranscript = '';
277
+
278
+ function toggleSpeechRecognition(currentText) {
279
+ if (!recognition) {
280
+ recognition = setupSpeechRecognition();
281
+ if (!recognition) return [currentText, 'Start Dictation'];
282
+ }
283
+
284
+ if (isRecording) {
285
+ recognition.stop();
286
+ isRecording = false;
287
+ return [currentText, 'Start Dictation'];
288
+ } else {
289
+ fullTranscript = currentText || '';
290
+
291
+ recognition.onresult = (event) => {
292
+ let interimTranscript = '';
293
+
294
+ for (let i = event.resultIndex; i < event.results.length; i++) {
295
+ const transcript = event.results[i][0].transcript;
296
+ if (event.results[i].isFinal) {
297
+ fullTranscript += (fullTranscript ? ' ' : '') + transcript;
298
+ } else {
299
+ interimTranscript += transcript;
300
+ }
301
+ }
302
+
303
+ const textarea = document.querySelector('.letter-box textarea');
304
+ if (textarea) {
305
+ textarea.value = fullTranscript + (interimTranscript ? ' ' + interimTranscript : '');
306
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
307
+ }
308
+ };
309
+
310
+ recognition.onerror = (event) => {
311
+ console.error('Speech recognition error:', event.error);
312
+ isRecording = false;
313
+ const button = document.querySelector('.mic-button');
314
+ if (button) button.value = 'Start Dictation';
315
+ };
316
+
317
+ recognition.onend = () => {
318
+ if (isRecording) {
319
+ recognition.start();
320
+ }
321
+ };
322
+
323
+ recognition.start();
324
+ isRecording = true;
325
+ return [fullTranscript, 'Stop Dictation'];
326
+ }
327
+ }
328
+ """
329
+
330
+ # Build Gradio interface
331
+ with gr.Blocks(css=custom_css, title="Pen Pal AI - Letter Exchange", theme=gr.themes.Soft()) as demo:
332
+ # Header image
333
+ gr.Image("demo-header.png", show_label=False, show_download_button=False, container=False)
334
+
335
+ gr.Markdown("""
336
+ Welcome to a different kind of AI conversation. Write thoughtful, long-form letters and receive detailed responses.
337
+
338
+ **How it works:**
339
+ 1. Set your OpenAI API key below (if not already configured)
340
+ 2. Write your letter in the text area
341
+ 3. Click "Send Letter" or use dictation
342
+ 4. Receive a reply from your AI pen pal
343
+ 5. Download any letter as markdown
344
+
345
+ This is asynchronous correspondence - take your time, be thoughtful, and enjoy the exchange!
346
+ """)
347
+
348
+ # API Key Configuration
349
+ with gr.Accordion("API Key Configuration (BYOK)", open=False):
350
+ gr.Markdown("""
351
+ This app requires an OpenAI API key. You can get one at [platform.openai.com](https://platform.openai.com/api-keys).
352
+
353
+ **For Hugging Face Spaces:** Set your key as a secret named `OPENAI_API_KEY` in your Space settings.
354
+
355
+ **For local use:** You can also set it here temporarily (not saved between sessions).
356
+ """)
357
+
358
+ with gr.Row():
359
+ api_key_input = gr.Textbox(
360
+ label="OpenAI API Key",
361
+ placeholder="sk-...",
362
+ type="password",
363
+ scale=3
364
+ )
365
+ set_key_btn = gr.Button("Set API Key", size="sm", scale=1)
366
+
367
+ api_key_status = gr.Markdown("")
368
+
369
+ # Hidden state for thread history
370
+ thread_state = gr.State([])
371
+
372
+ with gr.Row():
373
+ with gr.Column(scale=1):
374
+ compose_section_title = gr.Markdown("## Compose Your Letter")
375
+
376
+ user_input = gr.Textbox(
377
+ label="Your Letter",
378
+ placeholder="Dear AI Pen Pal,\n\nI've been thinking about...",
379
+ lines=15,
380
+ elem_classes=["letter-box"]
381
+ )
382
+
383
+ with gr.Row():
384
+ mic_btn = gr.Button("Start Dictation", size="sm", elem_classes=["mic-button"])
385
+
386
+ with gr.Row():
387
+ send_btn = gr.Button("Send Letter", variant="primary", size="lg")
388
+ new_conv_btn = gr.Button("New Conversation", size="lg")
389
+
390
+ gr.Markdown("---")
391
+
392
+ gr.Markdown("### Your Last Letter")
393
+ user_letter_display = gr.Markdown(
394
+ value="*Your letter will appear here after sending*",
395
+ elem_classes=["letter-display"]
396
+ )
397
+ download_user = gr.DownloadButton(
398
+ label="Download Your Letter",
399
+ size="sm"
400
+ )
401
+
402
+ with gr.Column(scale=1):
403
+ gr.Markdown("## AI Reply")
404
+
405
+ ai_response = gr.Markdown(
406
+ value="*Waiting for your letter...*",
407
+ elem_classes=["letter-display"]
408
+ )
409
+
410
+ download_ai = gr.DownloadButton(
411
+ label="Download AI Reply",
412
+ size="sm"
413
+ )
414
+
415
+ gr.Markdown("---")
416
+
417
+ gr.Markdown("## Letter Thread")
418
+ thread_display = gr.Markdown(
419
+ value="*No letters yet. Start writing!*",
420
+ elem_id="thread-history"
421
+ )
422
+
423
+ # Event handlers
424
+ set_key_btn.click(
425
+ fn=set_api_key,
426
+ inputs=[api_key_input],
427
+ outputs=[api_key_status]
428
+ )
429
+
430
+ send_btn.click(
431
+ fn=send_letter,
432
+ inputs=[user_input, thread_state],
433
+ outputs=[user_input, ai_response, thread_state, thread_display, user_letter_display, send_btn, user_input, compose_section_title]
434
+ )
435
+
436
+ new_conv_btn.click(
437
+ fn=new_conversation,
438
+ inputs=[],
439
+ outputs=[user_input, ai_response, thread_state, thread_display, user_letter_display, send_btn, user_input, compose_section_title]
440
+ )
441
+
442
+ # Speech-to-text handler
443
+ mic_btn.click(
444
+ fn=None,
445
+ inputs=[user_input],
446
+ outputs=[user_input, mic_btn],
447
+ js=speech_to_text_js.replace("function toggleSpeechRecognition", "function(currentText) { return toggleSpeechRecognition")
448
+ )
449
+
450
+ # Download handlers
451
+ user_letter_display.change(
452
+ fn=lambda x: gr.DownloadButton(
453
+ label="Download Your Letter",
454
+ value=x if x and x != "*Your letter will appear here after sending*" else None,
455
+ visible=bool(x and x != "*Your letter will appear here after sending*")
456
+ ),
457
+ inputs=[user_letter_display],
458
+ outputs=[download_user]
459
+ )
460
+
461
+ ai_response.change(
462
+ fn=lambda x: gr.DownloadButton(
463
+ label="Download AI Reply",
464
+ value=x if x and x != "*Waiting for your letter...*" else None,
465
+ visible=bool(x and x != "*Waiting for your letter...*")
466
+ ),
467
+ inputs=[ai_response],
468
+ outputs=[download_ai]
469
+ )
470
+
471
+ if __name__ == "__main__":
472
+ demo.launch(share=False)
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio==5.49.1
2
+ openai==2.6.0
3
+ python-dotenv==1.1.1