Spaces:
Sleeping
Sleeping
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>
- .env.example +1 -0
- .gitignore +14 -0
- CLAUDE.md +110 -0
- README.md +183 -1
- app.py +472 -0
- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+

|
| 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
|