Initial commit

This commit is contained in:
Aodhan Collins
2025-10-11 21:21:36 +01:00
commit eccd456c59
29 changed files with 5375 additions and 0 deletions

8
.env.example Normal file
View File

@@ -0,0 +1,8 @@
# OpenAI API Key (optional - for GPT models)
# Get your API key from: https://platform.openai.com/api-keys
OPENAI_API_KEY=your_openai_api_key_here
# OpenRouter API Key (optional - for Claude, Llama, Gemini, Mistral, etc.)
# Get your API key from: https://openrouter.ai/keys
# OpenRouter gives you access to 100+ LLMs with a single API key
OPENROUTER_API_KEY=your_openrouter_api_key_here

94
.gitignore vendored Normal file
View File

@@ -0,0 +1,94 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environments
.venv/
venv/
ENV/
env/
# Environment Variables (contains API keys)
.env
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
.project
.pydevproject
.settings/
*.sublime-project
*.sublime-workspace
# OS Files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
Desktop.ini
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
yarn.lock
# Frontend Build
frontend/build/
frontend/dist/
frontend/.cache/
# Logs
*.log
startup.log
logs/
# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
coverage/
# Jupyter Notebook
.ipynb_checkpoints
# Database
*.db
*.sqlite
*.sqlite3
# Temporary files
*.tmp
*.temp
.cache/
# Production
/static
/media

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.12

214
README.md Normal file
View File

@@ -0,0 +1,214 @@
# 🎭 Storyteller RPG Application
A storyteller-centric roleplaying application where characters communicate **privately** with the storyteller, who then crafts unique, personalized responses for each player.
## ✨ Key Features
- **🔒 Private Character-Storyteller Communication**: Each character has a completely isolated conversation with the storyteller - no character can see what others are saying or receiving
- **🎲 Storyteller-Centric Workflow**: The storyteller sees all character messages and responds to each individually
- **🤖 Multiple LLM Support**: Each character can use a different AI model (GPT-4, Claude, Llama, Gemini, Mistral, etc.) via OpenRouter or OpenAI
- **📜 Scene Narration**: Storyteller can broadcast scene descriptions visible to all characters
- **🧠 Isolated Memory Sessions**: Each character maintains their own separate conversation history with the storyteller
- **⚡ Real-time WebSocket Communication**: Instant message delivery
- **🎨 Modern, Beautiful UI**: Clean, intuitive interface with gradient themes
## 🎯 How It Works
1. **Storyteller creates a session** and shares the session ID
2. **Players join** by entering the session ID and creating their character
3. **Characters send private messages** to the storyteller (other players can't see these)
4. **Storyteller views all character messages** and crafts individual responses
5. **Each character receives their personalized response** from the storyteller
6. **Storyteller can narrate scenes** that all characters experience together
## 📋 Prerequisites
- Python 3.8+
- Node.js 16+
- npm or yarn
- **API Keys** (at least one):
- OpenAI API key (for GPT models)
- OpenRouter API key (for Claude, Llama, Gemini, Mistral, and 100+ other models)
## Setup
### Backend Setup
1. Create a virtual environment and activate it:
```bash
python -m venv venv
source venv/bin/activate # On Windows: .\venv\Scripts\activate
```
2. Install the required Python packages:
```bash
pip install -r requirements.txt
```
3. Set up your environment variables. Copy the example file and add your API keys:
```bash
cp .env.example .env
# Edit .env and add at least one API key:
# - OPENAI_API_KEY for GPT models
# - OPENROUTER_API_KEY for Claude, Llama, Gemini, etc.
```
**Get API Keys:**
- OpenAI: <https://platform.openai.com/api-keys>
- OpenRouter: <https://openrouter.ai/keys> (access 100+ models with one key!)
### Frontend Setup
1. Navigate to the frontend directory:
```bash
cd frontend
```
2. Install the required Node.js packages:
```bash
npm install
```
## Running the Application
### Start the Backend
From the project root directory:
```bash
uvicorn main:app --reload
```
The backend will be available at `http://localhost:8000`
### Start the Frontend
In a new terminal, navigate to the frontend directory and run:
```bash
cd frontend
npm start
```
The frontend will open in your default browser at `http://localhost:3000`
## 🎮 How to Use
### As the Storyteller
1. **Create a Session**
- Enter a session name and click "Create New Session"
- Copy the session ID and share it with your players
2. **Storyteller Dashboard**
- **Character List**: See all characters who have joined (left panel)
- **Pending Indicators**: Red badges show which characters are waiting for responses
- **Narrate Scenes**: Use the scene input at the top to describe events all characters experience
- **Private Conversations**: Click a character to view their conversation
- **Respond Individually**: Craft unique responses for each character privately
### As a Player/Character
1. **Join a Session**
- Get the session ID from your storyteller
- Enter your character name, description, and optional personality traits
- **Choose an AI model** for your character:
- **GPT-4o**: Latest OpenAI model, excellent all-around
- **Claude 3.5 Sonnet**: Creative, nuanced, great for roleplay
- **Llama 3.1**: Fast and free-spirited
- **Gemini Pro**: Google's powerful model
- Each model has unique personality traits!
- Click "Join Session"
2. **Playing Your Character**
- Send private messages to the storyteller
- **Your messages are private** - other players cannot see them
- Receive personalized responses from the storyteller (powered by your chosen LLM)
- See scene narrations that the storyteller broadcasts to everyone
- Your conversation history with the storyteller is preserved
## 🏗️ Architecture
- **Backend**: FastAPI with WebSockets for real-time bidirectional communication
- **Frontend**: React with modern component-based architecture
- **AI**: Multi-LLM support via:
- **OpenAI API**: GPT-4o, GPT-4 Turbo, GPT-3.5 Turbo
- **OpenRouter API**: Claude (Anthropic), Llama (Meta), Gemini (Google), Mistral, Cohere, and 100+ more
- Each character can use a different model for unique personalities
- **Memory**: In-memory storage with isolated conversation histories per character
- **Communication**:
- Separate WebSocket endpoints for characters (`/ws/character/{session_id}/{character_id}`) and storyteller (`/ws/storyteller/{session_id}`)
- Messages are routed privately between storyteller and individual characters
- Scene narrations are broadcast to all connected characters
## 🔐 Privacy Model
Each character has a **completely isolated** conversation with the storyteller:
- ✅ Character A cannot see Character B's messages
- ✅ Character A cannot see Character B's responses from the storyteller
- ✅ Each character only sees their own private conversation + scene narrations
- ✅ Only the storyteller sees all character conversations
## 🚀 Future Improvements
- [ ] Add user authentication and session persistence
- [ ] Database integration (PostgreSQL/MongoDB) for storing sessions
- [ ] Character sheets with stats and inventory
- [ ] Dice rolling mechanics with visual animations
- [ ] Support for multiple AI models (Claude, Llama, etc.)
- [ ] Chat history export/import
- [ ] Audio/voice integration for immersive experience
- [ ] Mobile app versions
- [ ] Group conversations (optional feature for party discussions)
- [ ] Image generation for scenes and characters
## 🔧 API Endpoints
### REST Endpoints
- `POST /sessions/` - Create a new game session
- `GET /sessions/{session_id}` - Get session details
- `POST /sessions/{session_id}/characters/` - Add a character to a session
- `GET /sessions/{session_id}/characters/{character_id}/conversation` - Get character's conversation history
- `POST /sessions/{session_id}/generate_suggestion` - Get AI suggestion for storyteller response (requires OpenAI API key)
### WebSocket Endpoints
- `ws/character/{session_id}/{character_id}` - Character's private connection
- `ws/storyteller/{session_id}` - Storyteller's connection to manage the session
### Model Management
- `GET /models` - Get list of available LLM models (based on configured API keys)
## 📚 Documentation
All project documentation is organized in the [`docs/`](./docs/) folder:
- **[Setup Guides](./docs/setup/)** - Quick start and installation
- **[Planning](./docs/planning/)** - Roadmaps and feature plans
- **[Reference](./docs/reference/)** - Technical guides and file references
- **[Development](./docs/development/)** - Session notes and implementation details
See [docs/README.md](./docs/README.md) for the complete documentation index.
## 🤖 Using Different LLMs
See [docs/reference/LLM_GUIDE.md](./docs/reference/LLM_GUIDE.md) for detailed information about:
- Available models and their personalities
- Cost comparisons
- Best practices for mixing models
- Setting up API keys
- Example party compositions
**Quick Summary:**
- Each character can use a different AI model
- OpenAI: GPT-4o, GPT-4, GPT-3.5
- OpenRouter: Claude, Llama, Gemini, Mistral, and 100+ more
- Different models = different personalities and response styles
- Creates emergent gameplay with unique character interactions
## License
MIT

32
dev.sh Executable file
View File

@@ -0,0 +1,32 @@
#!/bin/bash
# Development mode - starts servers in separate terminals
# Requires tmux or you can use separate terminal tabs
echo "🎭 Starting Storyteller RPG in Development Mode..."
echo ""
# Check if tmux is available
if command -v tmux &> /dev/null; then
echo "Using tmux for split terminals..."
# Create a new tmux session
tmux new-session -d -s storyteller
# Split the window
tmux split-window -h -t storyteller
# Run backend in left pane
tmux send-keys -t storyteller:0.0 'source .venv/bin/activate && python main.py' C-m
# Run frontend in right pane
tmux send-keys -t storyteller:0.1 'cd frontend && npm start' C-m
# Attach to the session
tmux attach -t storyteller
else
echo "tmux not found. Please install tmux or run servers in separate terminals:"
echo ""
echo "Terminal 1: python main.py"
echo "Terminal 2: cd frontend && npm start"
fi

67
docs/README.md Normal file
View File

@@ -0,0 +1,67 @@
# 📚 Storyteller RPG - Documentation
Welcome to the Storyteller RPG documentation. All project documentation is organized here by category.
---
## 📁 Documentation Structure
### 🚀 [setup/](./setup/)
**Getting started guides and quick references**
- **[QUICKSTART.md](./setup/QUICKSTART.md)** - 5-minute quick start guide
- **[QUICK_REFERENCE.md](./setup/QUICK_REFERENCE.md)** - Quick reference for common tasks
### 📋 [planning/](./planning/)
**Product roadmaps and feature planning**
- **[MVP_ROADMAP.md](./planning/MVP_ROADMAP.md)** - MVP feature requirements and roadmap
- **[NEXT_STEPS.md](./planning/NEXT_STEPS.md)** - Detailed future development roadmap
- **[PROJECT_PLAN.md](./planning/PROJECT_PLAN.md)** - Overall project planning and goals
### 📖 [reference/](./reference/)
**Technical references and guides**
- **[LLM_GUIDE.md](./reference/LLM_GUIDE.md)** - Guide to available LLM models
- **[PROJECT_FILES_REFERENCE.md](./reference/PROJECT_FILES_REFERENCE.md)** - Complete file structure reference
### 🔧 [development/](./development/)
**Development session notes and implementation details**
- **[SESSION_SUMMARY.md](./development/SESSION_SUMMARY.md)** - Complete development session summary
- **[IMPLEMENTATION_SUMMARY.md](./development/IMPLEMENTATION_SUMMARY.md)** - Technical implementation details
---
## 🎯 Quick Navigation
**New to the project?**
1. Start with the main [README.md](../README.md) in the root directory
2. Follow [setup/QUICKSTART.md](./setup/QUICKSTART.md) to get running
3. Review [planning/MVP_ROADMAP.md](./planning/MVP_ROADMAP.md) to understand the vision
**Want to contribute?**
1. Read [development/SESSION_SUMMARY.md](./development/SESSION_SUMMARY.md) for architecture
2. Check [planning/NEXT_STEPS.md](./planning/NEXT_STEPS.md) for feature priorities
3. Refer to [reference/PROJECT_FILES_REFERENCE.md](./reference/PROJECT_FILES_REFERENCE.md) for code navigation
**Looking for specific info?**
- **Setup/Installation** → [setup/](./setup/)
- **Features & Roadmap** → [planning/](./planning/)
- **API/Models/Files** → [reference/](./reference/)
- **Architecture** → [development/](./development/)
---
## 📊 Documentation Overview
| Category | Files | Purpose |
|----------|-------|---------|
| **Setup** | 2 | Getting started and quick references |
| **Planning** | 3 | Roadmaps, feature plans, project goals |
| **Reference** | 2 | Technical guides and file references |
| **Development** | 2 | Session notes and implementation details |
---
*Last updated: October 11, 2025*

View File

@@ -0,0 +1,126 @@
# Implementation Summary
## ✅ Completed Features
### Backend (`main.py`)
- **Isolated Character Sessions**: Each character has a separate conversation history that only they and the storyteller can see
- **Private WebSocket Channels**:
- `/ws/character/{session_id}/{character_id}` - Character's private connection
- `/ws/storyteller/{session_id}` - Storyteller's master connection
- **Message Routing**: Messages flow privately between storyteller and individual characters
- **Scene Broadcasting**: Storyteller can narrate scenes visible to all characters
- **Real-time Updates**: WebSocket events for character joins, messages, and responses
- **Pending Response Tracking**: System tracks which characters are waiting for storyteller responses
- **AI Suggestions** (Optional): Endpoint for AI-assisted storyteller response generation
### Frontend Components
#### 1. **SessionSetup.js**
- Create new session (storyteller)
- Join existing session (character)
- Character creation with name, description, and personality
- Beautiful gradient UI with modern styling
#### 2. **CharacterView.js**
- Private chat interface with storyteller
- Real-time message delivery via WebSocket
- Scene narration display
- Conversation history preservation
- Connection status indicator
#### 3. **StorytellerView.js**
- Dashboard showing all characters
- Character list with pending response indicators
- Click character to view their private conversation
- Individual response system for each character
- Scene narration broadcast to all characters
- Visual indicators for pending messages
### Styling (`App.css`)
- Modern gradient theme (purple/blue)
- Responsive design
- Smooth animations and transitions
- Clear visual hierarchy
- Mobile-friendly layout
### Documentation
- **README.md**: Comprehensive guide with architecture, features, and API docs
- **QUICKSTART.md**: Fast setup and testing guide
- **.env.example**: Environment variable template
## 🔐 Privacy Implementation
The core requirement - **isolated character sessions** - is implemented through:
1. **Separate Data Structures**: Each character has `conversation_history: List[Message]`
2. **WebSocket Isolation**: Separate WebSocket connections per character
3. **Message Routing**: Messages only sent to intended recipient
4. **Storyteller View**: Only storyteller can see all conversations
5. **Scene Broadcast**: Shared narrations go to all, but conversations stay private
## 🎯 Workflow
```
Character A → Storyteller: "I search the room"
Character B → Storyteller: "I attack the guard"
Storyteller sees both messages separately
Storyteller → Character A: "You find a hidden key"
Storyteller → Character B: "You miss your swing"
Character A only sees their conversation
Character B only sees their conversation
```
## 📁 File Structure
```
windsurf-project/
├── main.py # FastAPI backend with WebSocket support
├── requirements.txt # Python dependencies
├── .env.example # Environment template
├── README.md # Full documentation
├── QUICKSTART.md # Quick start guide
├── IMPLEMENTATION_SUMMARY.md # This file
└── frontend/
├── package.json
└── src/
├── App.js # Main app router
├── App.css # All styling
└── components/
├── SessionSetup.js # Session creation/join
├── CharacterView.js # Character interface
└── StorytellerView.js # Storyteller dashboard
```
## 🚀 To Run
**Backend:**
```bash
python main.py
```
**Frontend:**
```bash
cd frontend && npm start
```
## 🎨 Design Decisions
1. **WebSocket over REST**: Real-time bidirectional communication required for instant message delivery
2. **In-Memory Storage**: Simple session management; can be replaced with database for production
3. **Component-Based Frontend**: Separate views for different roles (setup, character, storyteller)
4. **Message Model**: Includes sender, content, timestamp for rich conversation history
5. **Pending Response Flag**: Helps storyteller track which characters need attention
## 🔮 Future Enhancements
- Database persistence (PostgreSQL/MongoDB)
- User authentication
- Character sheets with stats
- Dice rolling system
- Voice/audio support
- Mobile apps
- Multi-storyteller support
- Group chat rooms (for party discussions)

View File

@@ -0,0 +1,502 @@
# 📝 Development Session Summary
**Date:** October 11, 2025
**Project:** Storyteller RPG Application
**Status:** ✅ Fully Functional MVP Complete
---
## 🎯 Project Overview
Built a **storyteller-centric roleplaying application** where multiple AI character bots or human players interact with a storyteller through **completely isolated, private conversations**.
### Core Concept
- **Characters communicate ONLY with the storyteller** (never with each other by default)
- **Each character has separate memory/LLM sessions** - their responses are isolated
- **Storyteller sees all conversations** but responds to each character individually
- **Characters cannot see other characters' messages or responses**
- Characters can use **different AI models** (GPT-4, Claude, Llama, etc.) giving each unique personalities
---
## 🏗️ Architecture Built
### Backend: FastAPI + WebSockets
**File:** `/home/aodhan/projects/apps/storyteller/main.py` (398 lines)
**Key Components:**
1. **Data Models:**
- `GameSession` - Manages the game session and all characters
- `Character` - Stores character info, LLM model, and private conversation history
- `Message` - Individual message with sender, content, timestamp
- `ConnectionManager` - Handles WebSocket connections
2. **WebSocket Endpoints:**
- `/ws/character/{session_id}/{character_id}` - Private character connection
- `/ws/storyteller/{session_id}` - Storyteller dashboard connection
3. **REST Endpoints:**
- `POST /sessions/` - Create new game session
- `GET /sessions/{session_id}` - Get session details
- `POST /sessions/{session_id}/characters/` - Add character to session
- `GET /sessions/{session_id}/characters/{character_id}/conversation` - Get conversation history
- `POST /sessions/{session_id}/generate_suggestion` - AI-assisted storyteller responses
- `GET /models` - List available LLM models
4. **LLM Integration:**
- **OpenAI**: GPT-4o, GPT-4 Turbo, GPT-3.5 Turbo
- **OpenRouter**: Claude 3.5, Llama 3.1, Gemini Pro, Mistral, Cohere, 100+ models
- `call_llm()` function routes to appropriate provider based on model ID
- Each character can use a different model
5. **Message Flow:**
```
Character sends message → WebSocket → Stored in Character.conversation_history
Forwarded to Storyteller
Storyteller responds → WebSocket → Stored in Character.conversation_history
Sent ONLY to that Character
```
### Frontend: React
**Files:**
- `frontend/src/App.js` - Main router component
- `frontend/src/components/SessionSetup.js` (180 lines) - Session creation/joining
- `frontend/src/components/CharacterView.js` (141 lines) - Character interface
- `frontend/src/components/StorytellerView.js` (243 lines) - Storyteller dashboard
- `frontend/src/App.css` (704 lines) - Complete styling
**Key Features:**
1. **SessionSetup Component:**
- Create new session (becomes storyteller)
- Join existing session (becomes character)
- Select LLM model for character
- Model selector fetches available models from backend
2. **CharacterView Component:**
- Private conversation with storyteller
- WebSocket connection for real-time updates
- See scene narrations from storyteller
- Character info display (name, description, personality)
- Connection status indicator
3. **StorytellerView Component:**
- Dashboard showing all characters
- Click character to view their private conversation
- Respond to characters individually
- Narrate scenes visible to all characters
- Pending response indicators (red badges)
- Character cards showing:
- Name, description, personality
- LLM model being used
- Message count
- Pending status
4. **UI/UX Design:**
- Beautiful gradient purple theme
- Responsive design
- Real-time message updates
- Auto-scroll to latest messages
- Clear visual distinction between sent/received messages
- Session ID prominently displayed for sharing
- Empty states with helpful instructions
---
## 🔑 Key Technical Decisions
### 1. **Isolated Conversations (Privacy-First)**
- Each `Character` object has its own `conversation_history: List[Message]`
- Messages are never broadcast to all clients
- WebSocket routing ensures messages only go to intended recipient
- Storyteller has separate WebSocket endpoint to see all
### 2. **Multi-LLM Support**
- Characters choose model at creation time
- Stored in `Character.llm_model` field
- Backend dynamically routes API calls based on model prefix:
- `gpt-*` → OpenAI API
- Everything else → OpenRouter API
- Enables creative gameplay with different AI personalities
### 3. **In-Memory Storage (Current)**
- `sessions: Dict[str, GameSession]` stores all active sessions
- Fast and simple for MVP
- **Limitation:** Data lost on server restart
- **Next step:** Add database persistence (see NEXT_STEPS.md)
### 4. **WebSocket-First Architecture**
- Real-time bidirectional communication
- Native WebSocket API (not socket.io)
- JSON message format with `type` field for routing
- Separate connections for characters and storyteller
### 5. **Scene Narration System**
- Storyteller can broadcast "scene" messages
- Sent to all connected characters simultaneously
- Stored in `GameSession.current_scene` and `scene_history`
- Different from private character-storyteller messages
---
## 📁 Project Structure
```
storyteller/
├── main.py # FastAPI backend (398 lines)
├── requirements.txt # Python dependencies
├── .env.example # API key template
├── .env # Your API keys (gitignored)
├── README.md # Comprehensive documentation
├── QUICKSTART.md # 5-minute setup guide
├── NEXT_STEPS.md # Future development roadmap
├── SESSION_SUMMARY.md # This file
├── start.sh # Auto-start script
├── dev.sh # Development mode script
└── frontend/
├── package.json # Node dependencies
├── public/
│ └── index.html # HTML template
└── src/
├── App.js # Main router
├── App.css # All styles (704 lines)
├── index.js # React entry point
└── components/
├── SessionSetup.js # Session creation/joining
├── CharacterView.js # Character interface
└── StorytellerView.js # Storyteller dashboard
```
---
## 🚀 How to Run
### Quick Start (Automated)
```bash
cd /home/aodhan/projects/apps/storyteller
chmod +x start.sh
./start.sh
```
### Manual Start
```bash
# Terminal 1 - Backend
cd /home/aodhan/projects/apps/storyteller
source .venv/bin/activate # or: source venv/bin/activate
python main.py
# Terminal 2 - Frontend
cd /home/aodhan/projects/apps/storyteller/frontend
npm start
```
### Environment Setup
```bash
# Copy example and add your API keys
cp .env.example .env
# Edit .env and add at least one:
# OPENAI_API_KEY=sk-... # For GPT models
# OPENROUTER_API_KEY=sk-... # For Claude, Llama, etc.
```
---
## 🔍 Important Implementation Details
### WebSocket Message Types
**Character → Storyteller:**
```json
{
"type": "message",
"content": "I search the room for clues"
}
```
**Storyteller → Character:**
```json
{
"type": "storyteller_response",
"message": {
"id": "...",
"sender": "storyteller",
"content": "You find a hidden letter",
"timestamp": "2025-10-11T20:30:00"
}
}
```
**Storyteller → All Characters:**
```json
{
"type": "narrate_scene",
"content": "The room grows dark as thunder rumbles"
}
```
**Storyteller receives character message:**
```json
{
"type": "character_message",
"character_id": "uuid",
"character_name": "Aragorn",
"message": { ... }
}
```
**Character joined notification:**
```json
{
"type": "character_joined",
"character": {
"id": "uuid",
"name": "Legolas",
"description": "...",
"llm_model": "gpt-4"
}
}
```
### LLM Integration
**Function:** `call_llm(model, messages, temperature, max_tokens)`
**Routing Logic:**
```python
if model.startswith("gpt-") or model.startswith("o1-"):
# Use OpenAI client
response = await client.chat.completions.create(...)
else:
# Use OpenRouter via httpx
response = await http_client.post("https://openrouter.ai/api/v1/chat/completions", ...)
```
**Available Models (as of this session):**
- OpenAI: gpt-4o, gpt-4-turbo, gpt-4, gpt-3.5-turbo
- Anthropic (via OpenRouter): claude-3.5-sonnet, claude-3-opus, claude-3-haiku
- Meta: llama-3.1-70b, llama-3.1-8b
- Google: gemini-pro-1.5
- Mistral: mistral-large
- Cohere: command-r-plus
---
## 🎨 UI/UX Highlights
### Color Scheme
- Primary gradient: Purple (`#667eea` → `#764ba2`)
- Background: White cards on gradient
- Messages: Blue (sent) / Gray (received)
- Pending indicators: Red badges
- Status: Green (connected) / Gray (disconnected)
### Key UX Features
1. **Session ID prominently displayed** for easy sharing
2. **Pending response badges** show storyteller which characters are waiting
3. **Character cards** with all relevant info at a glance
4. **Empty states** guide users on what to do next
5. **Connection status** always visible
6. **Auto-scroll** to latest message
7. **Keyboard shortcuts** (Enter to send)
8. **Model selector** with descriptions helping users choose
---
## 🐛 Known Limitations & TODO
### Current Limitations
1. **No persistence** - Sessions lost on server restart
2. **No authentication** - Anyone with session ID can join
3. **No message editing/deletion** - Messages are permanent
4. **No character limit** on messages (could be abused)
5. **No rate limiting** - API calls not throttled
6. **No offline support** - Requires active connection
7. **No mobile optimization** - Works but could be better
8. **No sound notifications** - Easy to miss new messages
### Security Considerations
- **CORS is wide open** (`allow_origins=["*"]`) - Restrict in production
- **No input validation** on message content - Add sanitization
- **API keys in environment** - Good, but consider secrets manager
- **No session expiration** - Sessions live forever in memory
- **WebSocket not authenticated** - Anyone with session ID can connect
### Performance Considerations
- **In-memory storage** - Won't scale to many sessions
- **No message pagination** - All history loaded at once
- **No connection pooling** - Each character = new WebSocket
- **No caching** - LLM calls always go to API
---
## 💡 What Makes This Special
### Unique Features
1. **Each character uses a different AI model** - Creates emergent gameplay
2. **Completely private conversations** - True secret communication
3. **Storyteller-centric design** - Built for tabletop RPG flow
4. **Real-time updates** - Feels like a chat app
5. **Model flexibility** - 100+ LLMs via OpenRouter
6. **Zero configuration** - Works out of the box
### Design Philosophy
- **Storyteller is the hub** - All communication flows through them
- **Privacy first** - Characters truly can't see each other's messages
- **Flexibility** - Support for any LLM model
- **Simplicity** - Clean, intuitive interface
- **Real-time** - No page refreshes needed
---
## 🔄 Context for Continuing Development
### If Starting a New Chat Session
**What works:**
- ✅ Backend fully functional with all endpoints
- ✅ Frontend complete with all views
- ✅ WebSocket communication working
- ✅ Multi-LLM support implemented
- ✅ Scene narration working
- ✅ Private conversations isolated correctly
**Quick test to verify everything:**
```bash
# 1. Start servers
./start.sh
# 2. Create session as storyteller
# 3. Join session as character (new browser/incognito)
# 4. Send message from character
# 5. Verify storyteller sees it
# 6. Respond from storyteller
# 7. Verify character receives it
# 8. Test scene narration
```
**Common issues:**
- **Port 8000/3000 already in use** - `start.sh` kills existing processes
- **WebSocket won't connect** - Check backend is running, check browser console
- **LLM not responding** - Verify API keys in `.env`
- **npm/pip dependencies missing** - Run install commands
### Files to Modify for Common Tasks
**Add new WebSocket message type:**
1. Update message handler in `main.py` (character or storyteller endpoint)
2. Update frontend component to send/receive new type
**Add new REST endpoint:**
1. Add `@app.post()` or `@app.get()` in `main.py`
2. Add fetch call in appropriate frontend component
**Modify UI:**
1. Edit component in `frontend/src/components/`
2. Edit styles in `frontend/src/App.css`
**Add new LLM provider:**
1. Update `call_llm()` function in `main.py`
2. Update `get_available_models()` endpoint
3. Add model options in `SessionSetup.js`
---
## 📊 Project Statistics
- **Total Lines of Code:** ~1,700
- **Backend:** ~400 lines (Python/FastAPI)
- **Frontend:** ~1,300 lines (React/JavaScript/CSS)
- **Time to MVP:** 1 session
- **Dependencies:** 8 Python packages, 5 npm packages (core)
- **API Endpoints:** 6 REST + 2 WebSocket
- **React Components:** 3 main + 1 router
- **Supported LLMs:** 15+ models across 6 providers
---
## 🎓 Learning Resources Used
### Technologies
- **FastAPI:** https://fastapi.tiangolo.com/
- **WebSockets:** https://developer.mozilla.org/en-US/docs/Web/API/WebSocket
- **React:** https://react.dev/
- **OpenAI API:** https://platform.openai.com/docs
- **OpenRouter:** https://openrouter.ai/docs
### Key Concepts Implemented
- WebSocket bidirectional communication
- Async Python with FastAPI
- React state management with hooks
- Multi-provider LLM routing
- Real-time message delivery
- Isolated conversation contexts
---
## 📝 Notes for Future You
### Why certain decisions were made:
- **WebSocket instead of polling:** Real-time updates without constant HTTP requests
- **Separate endpoints for character/storyteller:** Clean separation of concerns, different message types
- **In-memory storage first:** Fastest MVP, can migrate to DB later
- **Multi-LLM from start:** Makes the app unique and interesting
- **No socket.io:** Native WebSocket simpler for this use case
- **Private conversations:** Core feature that differentiates from group chat apps
### What went smoothly:
- FastAPI made WebSocket implementation easy
- React components stayed clean and modular
- OpenRouter integration was straightforward
- UI came together nicely with gradients
### What could be improved:
- Database persistence is the obvious next step
- Error handling could be more robust
- Mobile experience needs work
- Need proper authentication system
- Testing suite would be valuable
---
## 🚀 Recommended Next Actions
**Immediate (Next Session):**
1. Test the app end-to-end to ensure everything works after IDE crash
2. Add AI suggestion button to storyteller UI (backend ready, just needs frontend)
3. Implement session persistence with SQLite
**Short Term (This Week):**
4. Add dice rolling system
5. Add typing indicators
6. Improve error messages
**Medium Term (This Month):**
7. Add authentication
8. Implement character sheets
9. Add image generation for scenes
See **NEXT_STEPS.md** for detailed roadmap with priorities and implementation notes.
---
## 📞 Session Handoff Checklist
- ✅ All files verified and up-to-date
- ✅ Architecture documented
- ✅ Key decisions explained
- ✅ Next steps outlined
- ✅ Common issues documented
- ✅ Code structure mapped
- ✅ API contracts specified
- ✅ Testing instructions provided
**You're ready to continue development!** 🎉
---
*Generated: October 11, 2025*
*Project Location: `/home/aodhan/projects/apps/storyteller`*
*Status: Production-ready MVP*

View File

@@ -0,0 +1,583 @@
# 🎯 Storyteller RPG - MVP Roadmap
**Goal:** Create a functional multi-user RPG system with human and AI players/storytellers, character profiles, and basic game management.
---
## 📊 Current Status
**✅ Completed:**
- Basic storyteller-player communication
- WebSocket real-time messaging
- Multiple LLM support (OpenAI + OpenRouter)
- Character-specific AI models
- Private character-storyteller conversations
**🔄 In Progress:**
- Enhanced user modes and permissions
---
## 🎯 MVP Feature Set
### Core User Modes
#### 1. **Player Mode** (Human & AI)
**Capabilities:**
- ✅ Create character with profile (race, class, gender, personality)
- ✅ Send messages to storyteller (public & private actions)
- ✅ Generate AI-assisted responses with custom prompts
- ✅ Edit/write custom messages before sending
- ✅ View storyteller responses and scene narrations
- ❌ Cannot edit storyteller responses
- ✅ Import/export character profiles (JSON & PNG with metadata)
**Message Types:**
1. **Public Action** - Visible to all players and storyteller
- Example: "I shake hands with the merchant"
2. **Private Action** - Only visible to storyteller
- Example: "I attempt to pickpocket him while shaking hands"
3. **Mixed Message** - Contains both public and private components
#### 2. **Storyteller Mode** (Human & AI)
**Capabilities:**
- ✅ View all player messages (public + private)
- ✅ Generate/edit responses to individual players
- ✅ Broadcast scene narrations to all players
- ✅ Control AI player responses
- ❌ Cannot edit human player messages
- ✅ Choose storytelling style (Narrator/DM/Internal)
- ✅ Set storyteller personality
**AI Storyteller:**
- Automatically generates responses to player actions
- Uses scenario/plot context to drive story
- Responds to all players based on their actions
- Can be overridden by Gamemaster
#### 3. **Gamemaster Mode** (Human Only)
**Capabilities:**
- ✅ Create and configure games
- ✅ Set player slots (human or AI)
- ✅ Assign storyteller (human or AI)
- ✅ View all game messages and state
- ✅ Send messages to any player/storyteller
- ✅ Edit AI responses (players and storyteller)
- ❌ Cannot edit human responses
- ✅ Direct commands to AI storyteller (via special channel)
- ✅ Save/load game progress
#### 4. **Admin Mode** (Development Only)
**Capabilities:**
- ✅ Full access to all games
- ✅ Edit any message or setting
- ✅ Manage LLM configuration
- ✅ View cost/usage analytics
- ✅ Debug mode features
---
## 🎭 Character Profile System (MVP)
### Character Creation Fields
```json
{
"name": "string",
"gender": "Male|Female|Non-binary|Custom",
"race": "Human|Elf|Dwarf|Orc|Halfling",
"class": "Warrior|Wizard|Cleric|Archer|Rogue",
"personality": "Friendly|Serious|Doubtful|Measured",
"llm_model": "string",
"custom_prompts": {
"character_background": "string",
"response_style": "string",
"special_traits": "string"
},
"appearance": {
"avatar": "base64_image_data",
"description": "string"
}
}
```
### Race Profiles (MVP)
#### **Human**
- **Traits:** Versatile, adaptable
- **LLM Prompt:** "You are a human character, balanced and adaptable to any situation."
#### **Elf**
- **Traits:** Graceful, perceptive, long-lived
- **LLM Prompt:** "You are an elf, graceful and wise with centuries of experience and keen senses."
#### **Dwarf**
- **Traits:** Sturdy, loyal, craftsman
- **LLM Prompt:** "You are a dwarf, stout and honorable with deep knowledge of stone and metal."
#### **Orc**
- **Traits:** Strong, fierce, honorable
- **LLM Prompt:** "You are an orc, powerful and direct with a strong sense of honor and combat."
#### **Halfling**
- **Traits:** Nimble, lucky, cheerful
- **LLM Prompt:** "You are a halfling, small but brave with natural luck and a cheerful disposition."
### Class Profiles (MVP)
#### **Warrior**
- **Focus:** Physical combat, leadership
- **Prompt:** "You excel in combat and tactics, preferring direct action and protecting your allies."
#### **Wizard**
- **Focus:** Arcane knowledge, spellcasting
- **Prompt:** "You are a master of arcane arts, solving problems with magic and knowledge."
#### **Cleric**
- **Focus:** Divine magic, healing, support
- **Prompt:** "You channel divine power to heal and protect, guided by faith and compassion."
#### **Archer**
- **Focus:** Ranged combat, precision, scouting
- **Prompt:** "You are a skilled marksman, preferring distance and precision in combat."
#### **Rogue**
- **Focus:** Stealth, cunning, skills
- **Prompt:** "You rely on stealth and cunning, using tricks and skills to overcome obstacles."
### Personality Profiles (MVP)
#### **Friendly**
- **Behavior:** Optimistic, trusting, cooperative
- **Prompt Modifier:** "You are friendly and approachable, always looking for the good in others."
#### **Serious**
- **Behavior:** Focused, pragmatic, disciplined
- **Prompt Modifier:** "You are serious and focused, prioritizing efficiency and practical solutions."
#### **Doubtful**
- **Behavior:** Cautious, questioning, analytical
- **Prompt Modifier:** "You are cautious and skeptical, questioning motives and analyzing situations carefully."
#### **Measured**
- **Behavior:** Balanced, thoughtful, diplomatic
- **Prompt Modifier:** "You are measured and thoughtful, weighing options carefully before acting."
### Character Import/Export
#### **JSON Export**
```json
{
"version": "1.0",
"character": { /* full character data */ },
"created_at": "2025-01-11T00:00:00Z",
"game_history": [ /* optional */ ]
}
```
#### **PNG Export with Metadata**
- Character avatar/portrait as PNG image
- Character data embedded in PNG metadata (EXIF/custom chunks)
- Allows sharing characters visually
- Import by uploading PNG file
**Libraries Needed:**
- Frontend: `exif-js`, `png-metadata`
- Backend: `Pillow` (Python) for PNG metadata
---
## 📜 Storyteller System (MVP)
### Storyteller Styles
#### **Narrator (3rd Person)**
```
Example: "The warrior steps forward, hand extended toward the merchant.
The shopkeeper's eyes gleam with suspicion as something feels amiss..."
```
**Prompt:** "Narrate the scene in third person, describing actions and atmosphere objectively."
#### **DM/Game Master (2nd Person)**
```
Example: "You step forward and shake the merchant's hand. As you do,
you notice his purse hanging loosely from his belt..."
```
**Prompt:** "Address players directly in second person, as a dungeon master guiding their experience."
#### **Internal Thoughts (1st Person)**
```
Example: "I watch as the warrior approaches. Something about his
movements seems off... Is he trying to distract me?"
```
**Prompt:** "Describe scenes from the perspective of NPCs or environment, revealing inner thoughts."
### Storyteller Personalities (MVP)
#### **Neutral**
- **Behavior:** Unbiased, balanced narration
- **Prompt:** "Narrate events fairly without favoring players or NPCs. Be descriptive but neutral."
*(Future: Add Challenging, Supportive, Mysterious personalities)*
---
## 🛠️ MVP Implementation Plan
### Phase 1: Enhanced Message System (Week 1-2)
**Tasks:**
1. **Public/Private Message Types**
- [ ] Update `Message` model to include `visibility` field (public/private/mixed)
- [ ] Frontend UI for selecting message visibility
- [ ] Filter messages based on user role/permissions
- [ ] WebSocket handling for different message types
2. **Message Composer Enhancement**
- [ ] Tabbed interface: Public Action | Private Action | Mixed
- [ ] Visual indicators for message type
- [ ] Preview of what each role sees
- [ ] AI generation respects visibility settings
**Backend Changes:**
```python
class Message(BaseModel):
id: str
sender_id: str
content: str
visibility: str # "public", "private", "mixed"
public_content: Optional[str] # for mixed messages
private_content: Optional[str] # for mixed messages
timestamp: datetime
```
**Frontend Components:**
```jsx
<MessageComposer
onSend={handleSend}
visibilityOptions={['public', 'private', 'mixed']}
aiAssist={true}
/>
```
---
### Phase 2: Character Profile System (Week 3-4)
**Tasks:**
1. **Character Profile Data Structure**
- [ ] Extend `Character` model with profile fields
- [ ] Profile creation wizard UI
- [ ] Dropdown selectors for race/class/personality/gender
- [ ] Custom prompt input fields
- [ ] Avatar upload/selection
2. **Profile-Based LLM Prompts**
- [ ] Build system prompt from character profile
- [ ] Combine race + class + personality traits
- [ ] Inject custom prompts into LLM requests
- [ ] Store prompt templates in database
3. **Import/Export System**
- [ ] Export character to JSON
- [ ] Export character to PNG with embedded metadata
- [ ] Import from JSON file
- [ ] Import from PNG file (extract metadata)
- [ ] Profile validation on import
**Backend Implementation:**
```python
class CharacterProfile(BaseModel):
gender: str
race: str # Human, Elf, Dwarf, Orc, Halfling
character_class: str # Warrior, Wizard, Cleric, Archer, Rogue
personality: str # Friendly, Serious, Doubtful, Measured
background: Optional[str]
custom_prompts: Dict[str, str]
avatar_data: Optional[str] # base64 encoded
def build_character_prompt(profile: CharacterProfile) -> str:
"""Builds LLM system prompt from character profile"""
race_prompt = RACE_PROMPTS[profile.race]
class_prompt = CLASS_PROMPTS[profile.character_class]
personality_prompt = PERSONALITY_PROMPTS[profile.personality]
return f"{race_prompt} {class_prompt} {personality_prompt} {profile.custom_prompts.get('response_style', '')}"
```
**Frontend Components:**
```jsx
<CharacterCreationWizard>
<Step1_BasicInfo />
<Step2_RaceSelection />
<Step3_ClassSelection />
<Step4_Personality />
<Step5_CustomPrompts />
<Step6_Avatar />
<Step7_Review />
</CharacterCreationWizard>
<CharacterImportExport
onExportJSON={handleJSONExport}
onExportPNG={handlePNGExport}
onImport={handleImport}
/>
```
---
### Phase 3: User Mode Interfaces (Week 5-7)
**Tasks:**
1. **Player Interface Refinement**
- [ ] Character sheet display
- [ ] Public/private message controls
- [ ] AI generation with character context
- [ ] Cannot edit storyteller responses (UI enforcement)
2. **Storyteller Dashboard**
- [ ] View all messages (public + private)
- [ ] Response composer per player
- [ ] AI player management
- [ ] Style selector (Narrator/DM/Internal)
- [ ] Cannot edit human player messages
3. **Gamemaster Control Panel**
- [ ] Game creation wizard
- [ ] Player slot assignment (human/AI)
- [ ] Storyteller assignment (human/AI)
- [ ] View all game state
- [ ] Direct AI storyteller commands
- [ ] Edit AI responses only
4. **Permission Enforcement**
- [ ] Backend permission decorators
- [ ] Frontend UI disabled states
- [ ] WebSocket message filtering
- [ ] API endpoint protection
**Permission Matrix:**
| Action | Player | Storyteller | Gamemaster | Admin |
|--------|--------|-------------|------------|-------|
| Read own messages | ✅ | ✅ | ✅ | ✅ |
| Read other players (public) | ✅ | ✅ | ✅ | ✅ |
| Read other players (private) | ❌ | ✅ | ✅ | ✅ |
| Edit own messages | ✅ | ✅ | ✅ | ✅ |
| Edit storyteller responses | ❌ | ✅ | ❌ | ✅ |
| Edit human player messages | ❌ | ❌ | ❌ | ✅ |
| Edit AI responses | ❌ | ✅ | ✅ | ✅ |
| Create game | ❌ | ❌ | ✅ | ✅ |
| Assign roles | ❌ | ❌ | ✅ | ✅ |
| Direct AI commands | ❌ | ❌ | ✅ | ✅ |
---
### Phase 4: AI Automation (Week 8-9)
**Tasks:**
1. **AI Player System**
- [ ] Automated response generation
- [ ] Use character profile in prompts
- [ ] Respond to storyteller automatically
- [ ] Configurable response delay
- [ ] Generate public AND private actions
2. **AI Storyteller System**
- [ ] Respond to all player actions
- [ ] Generate scene narrations
- [ ] Track plot progression
- [ ] Use storyteller style in responses
- [ ] Gamemaster command channel
3. **Automation Controls**
- [ ] Enable/disable AI automation per role
- [ ] Manual trigger for AI responses
- [ ] Review mode (generate but don't send)
- [ ] Override AI decisions
**AI Player Prompt Structure:**
```
System: You are {name}, a {gender} {race} {class} with a {personality} personality.
{race_traits}
{class_abilities}
{personality_traits}
{custom_background}
Current scene: {scene_description}
Recent events: {recent_context}
Respond to the storyteller's latest message. Consider:
- What would your character do publicly (visible to all)?
- What secret actions might you take (only storyteller sees)?
- Stay in character based on your race, class, and personality.
Format your response as:
PUBLIC: [what everyone sees]
PRIVATE: [secret actions only storyteller knows]
```
---
### Phase 5: Game Management & Save System (Week 10-11)
**Tasks:**
1. **Game Creation**
- [ ] Game setup wizard
- [ ] Scenario/setting description
- [ ] Player slot configuration
- [ ] Storyteller assignment
- [ ] Game lobby/waiting room
2. **Save/Load System**
- [ ] Export game state to JSON
- [ ] Import saved game
- [ ] Auto-save every N minutes
- [ ] Game checkpoints
- [ ] Game history log
3. **Database Implementation**
- [ ] Replace in-memory storage
- [ ] SQLite for development
- [ ] PostgreSQL for production
- [ ] Migration system (Alembic)
**Game State Schema:**
```python
class Game(Base):
__tablename__ = "games"
id: str (PK)
name: str
scenario: str
created_by: str (FK to User)
created_at: datetime
status: str # lobby, active, paused, completed
# Configuration
max_players: int
storyteller_id: Optional[str]
storyteller_is_ai: bool
# Relations
players: List[Player]
messages: List[Message]
checkpoints: List[GameCheckpoint]
```
---
### Phase 6: Polish & Testing (Week 12)
**Tasks:**
1. **UI/UX Polish**
- [ ] Consistent styling across all modes
- [ ] Loading states and animations
- [ ] Error handling and user feedback
- [ ] Responsive design
2. **Testing**
- [ ] Unit tests for character profiles
- [ ] Permission enforcement tests
- [ ] AI automation tests
- [ ] End-to-end game flow tests
3. **Documentation**
- [ ] User guide for each mode
- [ ] Character creation guide
- [ ] Game setup tutorial
- [ ] API documentation
4. **Performance**
- [ ] Optimize LLM calls
- [ ] WebSocket performance
- [ ] Database query optimization
- [ ] Caching strategy
---
## 📦 Dependencies to Add
### Backend
```txt
# requirements.txt additions
sqlalchemy==2.0.23
alembic==1.13.0
pillow==10.1.0 # For PNG metadata handling
```
### Frontend
```json
// package.json additions
"react-hook-form": "^7.48.0",
"zod": "^3.22.0",
"file-saver": "^2.0.5",
"png-metadata": "^1.0.0"
```
---
## 🎯 MVP Success Criteria
**The MVP is complete when:**
**Players can:**
- Create characters with full profiles (race/class/personality)
- Send public and private messages
- Use AI to generate in-character responses
- Export/import character profiles (JSON & PNG)
**Storytellers can:**
- View all player actions (public and private)
- Respond to players with chosen style
- Control AI players
- Generate scene narrations
**AI Players can:**
- Automatically generate responses with character personality
- Create both public and private actions
- Respond contextually to storyteller
**AI Storytellers can:**
- Run entire games automatically
- Respond to all player actions
- Generate appropriate scene narrations
- Accept Gamemaster override
**Gamemasters can:**
- Create and configure games
- Assign human/AI to roles
- View entire game state
- Command AI storyteller
- Save/load games
**System can:**
- Enforce all permissions correctly
- Save and load game progress
- Handle multiple concurrent games
- Track LLM usage and costs
---
## 🚀 Next Immediate Steps
### Week 1 - Day 1-3: Message System
1. Update `Message` model with visibility fields
2. Create message type selector UI
3. Implement message filtering logic
4. Test public/private message flow
### Week 1 - Day 4-7: Character Profiles
1. Design character profile schema
2. Create race/class/personality prompt templates
3. Build character creation wizard
4. Implement profile-based LLM prompts
**Ready to begin?** Let's start with Phase 1 - Enhanced Message System!
---
**Last Updated:** 2025-01-11
**Target MVP Completion:** 12 weeks
**Current Phase:** Planning → Implementation

390
docs/planning/NEXT_STEPS.md Normal file
View File

@@ -0,0 +1,390 @@
# 🚀 Next Steps for Storyteller RPG
## Immediate Improvements (Quick Wins)
### 1. AI-Assisted Storyteller Responses
**Priority: High | Effort: Low**
- [x] Backend endpoint exists (`/sessions/{session_id}/generate_suggestion`)
- [ ] Add "✨ AI Suggest" button to StorytellerView response textarea
- [ ] Show suggested response that storyteller can edit before sending
- [ ] Allow storyteller to regenerate if they don't like the suggestion
**Implementation:**
```javascript
// In StorytellerView.js - Add button next to "Send Private Response"
const getSuggestion = async () => {
const response = await fetch(
`${API_URL}/sessions/${sessionId}/generate_suggestion?character_id=${selectedCharacter}`,
{ method: 'POST' }
);
const data = await response.json();
setResponseText(data.suggestion);
};
```
### 2. Session Persistence
**Priority: High | Effort: Medium**
Currently, sessions exist only in memory. Add database storage:
**Option A: SQLite (Simplest)**
```bash
pip install sqlalchemy aiosqlite
```
**Option B: PostgreSQL (Production-ready)**
```bash
pip install sqlalchemy asyncpg psycopg2-binary
```
**Files to modify:**
- Create `database.py` for SQLAlchemy models
- Update `main.py` to use database instead of `sessions: Dict`
- Add session persistence on server restart
### 3. Better UI/UX Enhancements
**Priority: Medium | Effort: Low**
- [ ] Add typing indicators ("Storyteller is typing...")
- [ ] Add sound notifications for new messages
- [ ] Add markdown support in messages (bold, italic, etc.)
- [ ] Add character avatars (emoji selector or image upload)
- [ ] Add "Export conversation" button (save as JSON/text)
- [ ] Show timestamp for when character joined
- [ ] Add "Last active" indicator for characters
### 4. Dice Rolling System
**Priority: Medium | Effort: Medium**
Add RPG dice mechanics:
```javascript
// New component: DiceRoller.js
const rollDice = (notation) => {
// Parse notation like "2d6+3", "1d20", etc.
// Return result and show animation
};
```
**Features:**
- Character can request roll from storyteller
- Storyteller sees roll request and can approve
- Results visible to storyteller only (or optionally to character)
- Support for various dice (d4, d6, d8, d10, d12, d20, d100)
## Medium-Term Features
### 5. Character Sheets & Stats
**Priority: Medium | Effort: High**
Add RPG character management:
```python
# In main.py - extend Character model
class CharacterStats(BaseModel):
health: int = 100
max_health: int = 100
attributes: Dict[str, int] = {} # Strength, Dex, etc.
inventory: List[str] = []
skills: List[str] = []
```
**UI:**
- Character view shows their stats in a sidebar
- Storyteller can edit character stats
- Add stat modification history/log
### 6. Image Generation Integration
**Priority: Medium | Effort: Medium**
Integrate DALL-E or Stable Diffusion:
```python
# New endpoint in main.py
@app.post("/sessions/{session_id}/generate_image")
async def generate_scene_image(scene_description: str):
# Use OpenAI DALL-E or Replicate Stable Diffusion
# Return image URL
pass
```
**Use cases:**
- Generate scene illustrations
- Create character portraits
- Visualize items or locations
### 7. Multiple Storytellers / Co-GMs
**Priority: Low | Effort: Medium**
Allow multiple storytellers in one session:
- One primary storyteller
- Assistant storytellers can respond to characters
- Storytellers can see each other's responses
- Add permission system
### 8. Group Conversations
**Priority: Medium | Effort: Medium**
Allow characters to talk to each other (not just storyteller):
```python
class ConversationChannel(BaseModel):
id: str
name: str # "Party Chat", "Private: Alice & Bob"
participants: List[str] # character_ids
messages: List[Message] = []
```
**Features:**
- Storyteller creates channels
- Characters can be added/removed from channels
- Storyteller can see all channels (but optionally hide from other characters)
## Advanced Features
### 9. Voice Integration
**Priority: Low | Effort: High**
Add voice chat or text-to-speech:
**Option A: Text-to-Speech**
```python
# Use OpenAI TTS or ElevenLabs
@app.post("/tts")
async def text_to_speech(text: str, voice: str):
# Generate audio from text
pass
```
**Option B: Real-time Voice**
- Integrate WebRTC for voice channels
- Voice-to-text for automatic transcription
- Character AI can speak responses
### 10. Campaign Management
**Priority: Medium | Effort: High**
Add multi-session campaign support:
```python
class Campaign(BaseModel):
id: str
name: str
sessions: List[str] # session_ids
characters: Dict[str, Character] # Persistent across sessions
world_state: Dict[str, Any] # Locations, NPCs, quests
```
### 11. AI NPCs
**Priority: Medium | Effort: Medium**
Storyteller can create AI-controlled NPCs:
```python
class NPC(BaseModel):
id: str
name: str
description: str
ai_controlled: bool = True
llm_model: str = "gpt-3.5-turbo"
```
**Features:**
- NPCs have their own LLM sessions
- NPCs can respond to character messages
- Storyteller can override/edit NPC responses
- NPCs remember previous interactions
### 12. Combat System
**Priority: Low | Effort: High**
Add turn-based combat:
- Initiative tracking
- Action queue
- Status effects
- Health/damage tracking
- Combat log visible to all participants
## Infrastructure Improvements
### 13. Authentication & User Accounts
**Priority: High | Effort: High**
Add user system:
```bash
pip install python-jose[cryptography] passlib[bcrypt]
```
**Features:**
- User registration/login
- OAuth support (Google, Discord)
- User owns their characters
- User can be storyteller for multiple campaigns
- Session access control
### 14. Database Migration
**Priority: High | Effort: Medium**
Current: In-memory storage
Target: PostgreSQL or MongoDB
**Why:**
- Persist sessions across restarts
- Scale to multiple servers
- Backup and recovery
- Query optimization
### 15. API Rate Limiting
**Priority: Medium | Effort: Low**
Prevent abuse:
```python
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
@app.post("/sessions/")
@limiter.limit("10/minute")
async def create_session(...):
pass
```
### 16. WebSocket Reconnection
**Priority: Medium | Effort: Medium**
Handle disconnections gracefully:
- Auto-reconnect on connection loss
- Queue messages while disconnected
- Sync state on reconnection
- Show connection status prominently
### 17. Mobile App
**Priority: Low | Effort: Very High**
Build native mobile apps:
**Option A: React Native**
- Reuse React components
- iOS + Android from one codebase
**Option B: Progressive Web App (PWA)**
- Add service worker
- Offline support
- Add to home screen
- Push notifications
### 18. Testing Suite
**Priority: High | Effort: Medium**
Add comprehensive tests:
```bash
# Backend tests
pip install pytest pytest-asyncio httpx
# Frontend tests (already have Jest)
cd frontend
npm test
```
**Coverage:**
- Unit tests for message routing
- Integration tests for WebSocket connections
- End-to-end tests for full user flows
- Load testing for concurrent sessions
## Polish & Quality of Life
### 19. Better Error Handling
- Show user-friendly error messages
- Add retry logic for failed API calls
- Graceful degradation when LLM API is down
- Better validation on all inputs
### 20. Accessibility
- Add keyboard navigation
- Screen reader support
- High contrast mode
- Font size controls
- Color blind friendly themes
### 21. Internationalization (i18n)
- Support multiple languages
- Translate UI elements
- LLM can respond in user's language
### 22. Analytics & Monitoring
- Track session duration
- Monitor WebSocket health
- LLM API usage and costs
- User engagement metrics
## Documentation
### 23. API Documentation
- [ ] Add Swagger/OpenAPI docs (FastAPI auto-generates this)
- [ ] Document WebSocket message formats
- [ ] Add code examples for all endpoints
### 24. Tutorial System
- [ ] In-app tutorial for new users
- [ ] Video guides
- [ ] Example campaign/scenario
- [ ] Best practices guide for storytellers
## Prioritized Roadmap
### Phase 1: Core Stability (1-2 weeks)
1. ✅ AI-assisted storyteller responses (UI hookup)
2. ✅ Database persistence (SQLite or PostgreSQL)
3. ✅ Better error handling
4. ✅ WebSocket reconnection
### Phase 2: Enhanced Gameplay (2-3 weeks)
5. ✅ Dice rolling system
6. ✅ Character sheets & stats
7. ✅ Typing indicators
8. ✅ Export conversations
### Phase 3: Rich Features (3-4 weeks)
9. ✅ Image generation
10. ✅ Group conversations
11. ✅ AI NPCs
12. ✅ Authentication system
### Phase 4: Scale & Polish (2-3 weeks)
13. ✅ Production database
14. ✅ Testing suite
15. ✅ Mobile PWA
16. ✅ Campaign management
## Quick Wins to Start With
If you're continuing development, I recommend starting with:
1. **Add AI Suggest button** (30 mins) - The backend already supports it!
2. **Add typing indicators** (1 hour) - Great UX improvement
3. **Session persistence** (3-4 hours) - Prevents data loss
4. **Dice roller** (2-3 hours) - Core RPG feature
5. **Export conversations** (1 hour) - Users will love this
## Notes for Future Development
- All WebSocket messages use JSON format with a `type` field
- Character conversations are stored in `Character.conversation_history`
- Each character has their own LLM model configurable at creation
- Frontend uses native WebSocket (not socket.io despite the package.json)
- Backend runs on port 8000, frontend on port 3000
- CORS is wide open for development - restrict in production!
## Getting Help
- **FastAPI docs**: https://fastapi.tiangolo.com/
- **React docs**: https://react.dev/
- **OpenAI API**: https://platform.openai.com/docs
- **OpenRouter**: https://openrouter.ai/docs
- **WebSocket guide**: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket

View File

@@ -0,0 +1,525 @@
# 🎭 Storyteller RPG - Project Plan
## Vision
Transform the application into a full-featured RPG platform with multiple user roles, AI automation, and comprehensive game management capabilities.
---
## 📋 Phase 1: User Roles & Authentication System
### 1.1 Backend Infrastructure
- [ ] **User Model & Authentication**
- Implement user registration/login system
- Add JWT token-based authentication
- User profile management
- Session management with user association
- [ ] **Role-Based Access Control (RBAC)**
- Define role hierarchy: Admin > Gamemaster > Storyteller > Player
- Create permission decorators for API endpoints
- Implement role checking middleware
- Role assignment system
- [ ] **Database Integration**
- Replace in-memory storage with persistent database (SQLite/PostgreSQL)
- User table schema
- Game/Session table schema
- Character/Player table schema with user association
- Message history with ownership tracking
**Estimated Time:** 1-2 weeks
**Technical Stack:**
```python
# Backend additions needed:
- SQLAlchemy (ORM)
- JWT (python-jose)
- Password hashing (passlib)
- Database (SQLite for dev, PostgreSQL for prod)
```
---
## 📋 Phase 2: Game Creation & Management System
### 2.1 Game Configuration
- [ ] **Game Creation Interface**
- Game setup wizard for Gamemaster
- Configure game settings:
- Game name & description
- Scenario/setting details
- Number of player slots
- Storyteller assignment (human/AI)
- LLM model selection per role
- Game state management (not started, in progress, paused, completed)
- [ ] **Role Assignment System**
- Player slot management
- Assign slots to:
- Human users (invite by username/email)
- AI players (configure personality & LLM)
- Storyteller assignment (human or AI)
- Role transfer/reassignment
- [ ] **Game Lobby**
- Pre-game waiting room
- Player slot status display
- Ready/not ready indicators
- Game start conditions
**Estimated Time:** 2 weeks
**UI Components:**
```
- GameCreationWizard.js
- RoleAssignmentPanel.js
- GameLobby.js
- PlayerSlotCard.js
```
---
## 📋 Phase 3: User Mode Interfaces
### 3.1 Player Mode
- [ ] **Player Interface**
- View own character sheet
- Private messaging with storyteller
- View scene descriptions
- Message history (own messages only)
- Cannot edit storyteller responses
- Can generate/edit/write own responses
- [ ] **Player Controls**
- Message composer with AI generation option
- Edit own messages (before storyteller reads)
- Character action commands
- Inventory/stats display (if applicable)
**Permissions:**
```
✅ Read: Own messages, storyteller responses to self, scene narrations
✅ Write: Own character messages
✅ Edit: Own unread messages
❌ Edit: Storyteller responses, other players' messages
```
### 3.2 Storyteller Mode
- [ ] **Storyteller Dashboard**
- View all player messages
- Player list with status indicators
- Response composer per player
- Scene narration composer
- AI player response generation/editing
- [ ] **Storyteller Controls**
- Generate responses with AI assist (per player's LLM)
- Edit AI-generated responses before sending
- Manage AI players completely
- Broadcast scene narrations
- View conversation history per player
**Permissions:**
```
✅ Read: All player messages, all game state
✅ Write: Responses to players, scene narrations
✅ Edit: Own responses, AI player messages
❌ Edit: Human player messages
✅ Generate: AI responses for any player
```
### 3.3 Gamemaster Mode
- [ ] **Gamemaster Control Panel**
- Overview of entire game state
- All player & storyteller message threads
- Game configuration management
- Player/role management
- AI behavior controls
- [ ] **Gamemaster Powers**
- Send messages to any player
- Edit AI responses (storyteller or players)
- Pause/resume game
- Modify game settings mid-game
- View all LLM usage/costs
- Generate content with any LLM
**Permissions:**
```
✅ Read: Everything
✅ Write: Messages to anyone
✅ Edit: AI responses (storyteller & players)
❌ Edit: Human responses (storyteller & players)
✅ Configure: Game settings, roles, LLMs
✅ Control: AI automation, game flow
```
### 3.4 Admin Mode (Development)
- [ ] **Admin Dashboard**
- System-wide overview
- All games management
- User management
- LLM configuration & monitoring
- Cost tracking & analytics
- [ ] **Admin Powers**
- Full edit access to everything
- View/modify all LLMs in use
- System configuration
- Debug tools
- Performance monitoring
**Permissions:**
```
✅ Full Access: Everything, no restrictions
```
**Estimated Time:** 3-4 weeks
---
## 📋 Phase 4: AI Automation System
### 4.1 AI Player System
- [ ] **AI Player Engine**
- Automated message generation based on:
- Character personality/backstory
- Recent conversation context
- Scene descriptions
- Game scenario
- Configurable response triggers:
- Time-based (respond every N seconds)
- Event-based (respond to storyteller)
- Manual trigger (Gamemaster/Storyteller control)
- [ ] **AI Player Configuration**
- Personality templates
- Response style settings
- Behavior patterns (cautious, bold, curious, etc.)
- LLM model selection per AI player
- Temperature/creativity settings
### 4.2 AI Storyteller System
- [ ] **AI Storyteller Engine**
- Automated storytelling based on:
- Game scenario/plot
- Player actions
- Story progression rules
- Scene templates
- Response generation for each player
- Scene narration generation
- [ ] **AI Storyteller Configuration**
- Storytelling style (descriptive, action-focused, dialogue-heavy)
- Plot progression speed
- Challenge difficulty
- LLM model selection
- Context window management
### 4.3 Automation Controls
- [ ] **Manual Override System**
- Human can take over from AI at any time
- AI can be paused/resumed
- Edit AI responses before sending
- Review mode (AI generates, human approves)
- [ ] **Automation Triggers**
- Configurable response timing
- Event-based triggers
- Turn-based mode support
- Real-time mode support
**Estimated Time:** 2-3 weeks
**Technical Implementation:**
```python
# Backend components:
- AIPlayerEngine class
- AIStorytellerEngine class
- AutomationScheduler
- ResponseGenerator with context management
- Trigger system for automated responses
```
---
## 📋 Phase 5: Enhanced Features
### 5.1 Message Management
- [ ] **Message Editing System**
- Edit history tracking
- Permissions enforcement
- Real-time sync of edits
- Visual indicators for edited messages
- [ ] **Message Generation**
- AI-assisted writing for all roles
- Prompt templates library
- Context-aware suggestions
- Multi-model comparison
### 5.2 Game State Management
- [ ] **Save/Load System**
- Export game state
- Import saved games
- Game snapshots/checkpoints
- Rewind to previous state
- [ ] **Turn Management** (Optional)
- Turn-based gameplay option
- Turn order configuration
- Turn timer
- Turn history
### 5.3 Analytics & Monitoring
- [ ] **Usage Tracking**
- LLM usage per game
- Cost tracking per model
- Response time metrics
- Player engagement analytics
- [ ] **Game Insights**
- Conversation flow visualization
- Player participation metrics
- Story progression tracking
- AI vs Human response statistics
**Estimated Time:** 2 weeks
---
## 📋 Phase 6: Polish & Deployment
### 6.1 UI/UX Refinement
- [ ] Responsive design for all modes
- [ ] Mobile-friendly interfaces
- [ ] Accessibility improvements
- [ ] Theme customization
### 6.2 Testing
- [ ] Unit tests for all components
- [ ] Integration tests
- [ ] Role permission tests
- [ ] Load testing
### 6.3 Documentation
- [ ] User guides per role
- [ ] API documentation
- [ ] Deployment guide
- [ ] Developer documentation
### 6.4 Deployment
- [ ] Production environment setup
- [ ] Database migration system
- [ ] CI/CD pipeline
- [ ] Monitoring & logging
**Estimated Time:** 2 weeks
---
## 🗓️ Timeline Summary
| Phase | Description | Duration | Status |
|-------|-------------|----------|--------|
| **Phase 0** | Current MVP | - | ✅ Complete |
| **Phase 1** | User Roles & Auth | 1-2 weeks | 🔄 Planning |
| **Phase 2** | Game Management | 2 weeks | ⏳ Pending |
| **Phase 3** | User Modes | 3-4 weeks | ⏳ Pending |
| **Phase 4** | AI Automation | 2-3 weeks | ⏳ Pending |
| **Phase 5** | Enhanced Features | 2 weeks | ⏳ Pending |
| **Phase 6** | Polish & Deploy | 2 weeks | ⏳ Pending |
| **Total** | | **12-15 weeks** | |
---
## 🏗️ Technical Architecture Changes
### Current Architecture
```
Frontend (React) ←→ Backend (FastAPI) ←→ In-Memory Storage
WebSockets
```
### Target Architecture
```
Frontend (React)
├── Player Interface
├── Storyteller Interface
├── Gamemaster Interface
└── Admin Interface
API Gateway (FastAPI)
├── Authentication Layer (JWT)
├── Authorization Layer (RBAC)
└── WebSocket Manager
Business Logic
├── Game Engine
├── AI Player Engine
├── AI Storyteller Engine
└── Automation Scheduler
Data Layer
├── Database (PostgreSQL)
├── Redis (Caching/Sessions)
└── File Storage (Game States)
External Services
├── OpenAI API
├── OpenRouter API
└── Analytics Service
```
---
## 📦 New Dependencies Needed
### Backend
```python
# requirements.txt additions
sqlalchemy==2.0.23 # ORM
alembic==1.13.0 # Database migrations
psycopg2-binary==2.9.9 # PostgreSQL driver
python-jose[cryptography] # JWT (already added, needs activation)
passlib[bcrypt] # Password hashing (already added, needs activation)
redis==5.0.1 # Caching & session management
celery==5.3.4 # Task queue for AI automation
pydantic-settings==2.1.0 # Settings management
```
### Frontend
```json
// package.json additions
"@tanstack/react-query": "^5.0.0", // Data fetching
"zustand": "^4.4.0", // State management
"react-router-dom": "^6.20.0", // Routing (if not added)
"react-hook-form": "^7.48.0", // Form management
"zod": "^3.22.0", // Schema validation
"recharts": "^2.10.0", // Analytics charts
"@dnd-kit/core": "^6.1.0" // Drag-and-drop for role assignment
```
---
## 🎯 Success Criteria
### Phase 1 Complete When:
- ✅ Users can register/login
- ✅ Role-based access works
- ✅ Database stores all game data persistently
### Phase 2 Complete When:
- ✅ Gamemaster can create games
- ✅ Can assign humans/AI to roles
- ✅ Game lobby works with ready states
### Phase 3 Complete When:
- ✅ All 4 user modes functional
- ✅ Permissions enforced correctly
- ✅ UI matches role capabilities
### Phase 4 Complete When:
- ✅ AI players respond automatically
- ✅ AI storyteller can run full games
- ✅ Human override works seamlessly
### Phase 5 Complete When:
- ✅ Message editing works with permissions
- ✅ Save/load system functional
- ✅ Analytics dashboard complete
### Phase 6 Complete When:
- ✅ App deployed to production
- ✅ All tests passing
- ✅ Documentation complete
---
## 🚀 Getting Started - Phase 1
### Immediate Next Steps:
1. **Database Setup**
```bash
pip install sqlalchemy alembic psycopg2-binary
alembic init migrations
```
2. **Create Database Models**
- User model with roles
- Game model
- Character model with user_id
- Message model with ownership
3. **Implement Authentication**
- JWT token generation
- Login/register endpoints
- Protected routes
4. **Update Frontend**
- Login/register pages
- Auth state management
- Protected route wrapper
### Priority Tasks for Phase 1:
1. Database schema design
2. User authentication system
3. Role assignment logic
4. Update existing session system to use database
---
## 📝 Notes & Considerations
### Key Design Decisions:
- **Database:** PostgreSQL for production, SQLite for development
- **Auth:** JWT tokens with refresh mechanism
- **Real-time:** Continue using WebSockets, add auth to connections
- **AI Queue:** Celery for handling automated AI responses
- **Caching:** Redis for session data and frequently accessed game state
### Challenges to Address:
- **Race Conditions:** Multiple users editing same game state
- **AI Cost Control:** Budget limits per game/user
- **Scalability:** Support for multiple concurrent games
- **Context Management:** LLM context windows for long games
- **Latency:** AI response generation can be slow
### Future Enhancements (Post-MVP):
- Voice chat integration
- Character sheet system with stats/inventory
- Dice rolling system
- Battle/combat system
- Campaign management (multi-session games)
- Marketplace for game scenarios
- Community features (forums, game discovery)
---
## 🤝 Development Workflow
### Branching Strategy:
```
main (production)
├── develop (integration)
├── feature/phase1-auth
├── feature/phase2-game-creation
├── feature/phase3-player-mode
└── feature/phase4-ai-automation
```
### Testing Strategy:
- Unit tests for all new backend functions
- Integration tests for API endpoints
- E2E tests for critical user flows
- Load tests for AI automation
### Code Review Process:
- All features require PR review
- Test coverage minimum: 80%
- Documentation required for new features
---
**Last Updated:** 2025-10-11
**Next Review:** Start of Phase 1

166
docs/reference/LLM_GUIDE.md Normal file
View File

@@ -0,0 +1,166 @@
# 🤖 LLM Model Guide
## Overview
Each character in the storyteller RPG can use a different AI model, giving them unique personality traits and response styles. This creates incredible variety and emergent gameplay!
## Available Models
### OpenAI Models (requires OPENAI_API_KEY)
#### GPT-4o (Latest)
- **Best for**: All-around excellence, latest capabilities
- **Personality**: Intelligent, balanced, reliable
- **Cost**: $$$$
- **Speed**: Fast
#### GPT-4 Turbo
- **Best for**: Complex reasoning, detailed responses
- **Personality**: Thoughtful, articulate, analytical
- **Cost**: $$$
- **Speed**: Medium
#### GPT-3.5 Turbo
- **Best for**: Quick interactions, budget-friendly
- **Personality**: Energetic, conversational, casual
- **Cost**: $
- **Speed**: Very Fast
### OpenRouter Models (requires OPENROUTER_API_KEY)
#### Claude 3.5 Sonnet (Anthropic)
- **Best for**: Creative roleplay, nuanced responses
- **Personality**: Thoughtful, creative, emotionally aware
- **Cost**: $$$
- **Speed**: Fast
- **Great for**: Characters with depth and complexity
#### Claude 3 Opus (Anthropic)
- **Best for**: Most sophisticated responses
- **Personality**: Highly intelligent, philosophical
- **Cost**: $$$$
- **Speed**: Medium
- **Great for**: Wise characters, strategists
#### Claude 3 Haiku (Anthropic)
- **Best for**: Quick, concise responses
- **Personality**: Efficient, direct, clever
- **Cost**: $
- **Speed**: Very Fast
- **Great for**: Action-oriented characters
#### Gemini Pro 1.5 (Google)
- **Best for**: Factual knowledge, analytical thinking
- **Personality**: Logical, informative, precise
- **Cost**: $$
- **Speed**: Fast
- **Great for**: Scholarly characters, investigators
#### Llama 3.1 70B (Meta)
- **Best for**: Free-spirited, creative responses
- **Personality**: Bold, spontaneous, unpredictable
- **Cost**: $$
- **Speed**: Medium
- **Great for**: Wild characters, rogues
#### Llama 3.1 8B (Meta)
- **Best for**: Fast, lightweight interactions
- **Personality**: Quick-witted, energetic
- **Cost**: $
- **Speed**: Very Fast
- **Great for**: Nimble characters, scouts
#### Mistral Large (Mistral AI)
- **Best for**: European flair, multilingual
- **Personality**: Cultured, articulate, sophisticated
- **Cost**: $$
- **Speed**: Fast
- **Great for**: Noble characters, diplomats
#### Command R+ (Cohere)
- **Best for**: Following instructions precisely
- **Personality**: Obedient, structured, methodical
- **Cost**: $$
- **Speed**: Fast
- **Great for**: Soldiers, servants, loyal companions
## Mixing Models for Rich Gameplay
### Example Party Compositions
#### The Diverse Adventurers
- **Wise Wizard**: Claude 3 Opus (philosophical, strategic)
- **Brave Warrior**: GPT-4 Turbo (tactical, heroic)
- **Sneaky Rogue**: Llama 3.1 70B (unpredictable, bold)
- **Scholar**: Gemini Pro (analytical, knowledgeable)
#### The Quick Squad
- **Scout**: Llama 3.1 8B (fast, energetic)
- **Fighter**: Claude 3 Haiku (direct, efficient)
- **Mage**: GPT-3.5 Turbo (quick casting)
#### The Elite Team
- **Leader**: GPT-4o (balanced, excellent)
- **Advisor**: Claude 3.5 Sonnet (creative strategy)
- **Specialist**: Gemini Pro (expert knowledge)
## Cost Considerations
Models are charged per token (roughly per word). Approximate costs:
- **$**: ~$0.50-1 per 1000 messages
- **$$**: ~$1-3 per 1000 messages
- **$$$**: ~$3-10 per 1000 messages
- **$$$$**: ~$10-30 per 1000 messages
**Tips to save money:**
- Use cheaper models (GPT-3.5, Claude Haiku, Llama 8B) for most characters
- Reserve expensive models (GPT-4o, Claude Opus) for key NPCs or special moments
- Mix and match based on character importance
## Setting Up API Keys
### OpenAI
1. Create account at <https://platform.openai.com>
2. Add payment method
3. Generate API key at <https://platform.openai.com/api-keys>
4. Add to `.env`: `OPENAI_API_KEY=sk-...`
### OpenRouter
1. Create account at <https://openrouter.ai>
2. Add credits (starts at $5)
3. Generate API key at <https://openrouter.ai/keys>
4. Add to `.env`: `OPENROUTER_API_KEY=sk-...`
**Why OpenRouter?**
- Single API key for 100+ models
- Pay-as-you-go pricing
- No monthly subscriptions
- Access to Claude, Llama, Gemini, Mistral, and more
## Testing Models
Try creating characters with different models and see how they respond differently to the same situation!
Example test:
1. Create 3 characters with different models
2. Have all three search a mysterious room
3. Compare their unique approaches:
- GPT-4: Methodical, detailed search
- Claude: Creative interpretations
- Llama: Bold, risky actions
- Gemini: Logical deductions
## Advanced: Custom Model Configuration
You can add more models by editing `main.py`:
```python
# In the get_available_models() function
models["openrouter"] = [
{"id": "your/model-id", "name": "Display Name", "provider": "Provider"},
# Add more models here
]
```
Find model IDs at <https://openrouter.ai/models>

View File

@@ -0,0 +1,300 @@
# 📂 Project Files Reference
Quick reference guide to all files in the Storyteller RPG project.
---
## 🔧 Core Application Files
### Backend (Python/FastAPI)
| File | Lines | Purpose |
|------|-------|---------|
| **main.py** | 398 | Complete FastAPI backend with WebSocket support, LLM integration, and all API endpoints |
| **requirements.txt** | 9 | Python dependencies (FastAPI, OpenAI, httpx, etc.) |
### Frontend (React)
| File | Lines | Purpose |
|------|-------|---------|
| **frontend/src/App.js** | 40 | Main router component, manages session/character state |
| **frontend/src/App.css** | 704 | Complete styling for entire application |
| **frontend/src/index.js** | 12 | React entry point |
| **frontend/src/components/SessionSetup.js** | 180 | Session creation and joining interface with model selection |
| **frontend/src/components/CharacterView.js** | 141 | Character's private conversation interface |
| **frontend/src/components/StorytellerView.js** | 243 | Storyteller dashboard with character management |
| **frontend/public/index.html** | 18 | HTML template |
| **frontend/package.json** | 40 | Node.js dependencies and scripts |
---
## 📚 Documentation Files
| File | Purpose | When to Read |
|------|---------|--------------|
| **README.md** | Comprehensive project overview, features, setup instructions | First time setup or sharing with others |
| **QUICKSTART.md** | 5-minute quick start guide | When you want to run the app quickly |
| **SESSION_SUMMARY.md** | Complete development session summary, architecture, decisions | When continuing development or understanding the codebase |
| **NEXT_STEPS.md** | Detailed roadmap of future features and improvements | When planning next development phase |
| **PROJECT_FILES_REFERENCE.md** | This file - quick reference to all files | When navigating the project |
| **LLM_GUIDE.md** | Guide to available LLM models (if exists) | When choosing AI models for characters |
---
## 🔐 Configuration Files
| File | Purpose | Notes |
|------|---------|-------|
| **.env.example** | Template for environment variables | Copy to `.env` and add your API keys |
| **.env** | Your actual API keys | **GITIGNORED** - never commit this |
| **.python-version** | Python version specification | For pyenv users |
---
## 🚀 Scripts
| File | Purpose | Usage |
|------|---------|-------|
| **start.sh** | Auto-start both backend and frontend | `./start.sh` (Unix/Mac/Linux) |
| **start.bat** | Windows version of start script | `start.bat` (Windows) |
| **dev.sh** | Development mode with tmux split terminals | `./dev.sh` (requires tmux) |
---
## 📁 Directory Structure
```
storyteller/
├── 📄 Backend Files
│ ├── main.py # ⭐ Main backend application
│ └── requirements.txt # Python dependencies
├── 🌐 Frontend Files
│ └── frontend/
│ ├── src/
│ │ ├── App.js # ⭐ Main React component
│ │ ├── App.css # ⭐ All styles
│ │ ├── index.js # React entry point
│ │ └── components/
│ │ ├── SessionSetup.js # ⭐ Session creation/joining
│ │ ├── CharacterView.js # ⭐ Character interface
│ │ └── StorytellerView.js # ⭐ Storyteller dashboard
│ ├── public/
│ │ ├── index.html # HTML template
│ │ ├── manifest.json # PWA manifest
│ │ └── robots.txt # SEO robots
│ ├── package.json # Node dependencies
│ └── node_modules/ # Installed packages (auto-generated)
├── 📚 Documentation
│ ├── README.md # ⭐ Main documentation
│ ├── QUICKSTART.md # Quick setup guide
│ ├── SESSION_SUMMARY.md # ⭐ Development session summary
│ ├── NEXT_STEPS.md # ⭐ Future roadmap
│ └── PROJECT_FILES_REFERENCE.md # This file
├── 🔐 Configuration
│ ├── .env.example # Environment template
│ ├── .env # Your API keys (gitignored)
│ └── .python-version # Python version
└── 🚀 Scripts
├── start.sh # Unix/Mac/Linux launcher
├── start.bat # Windows launcher
└── dev.sh # Dev mode with tmux
```
---
## 🎯 Files by Priority
### Must Understand (Core Application)
1. **main.py** - Backend logic, WebSocket handling, LLM integration
2. **SessionSetup.js** - How users create/join sessions
3. **CharacterView.js** - Character's private conversation interface
4. **StorytellerView.js** - Storyteller's dashboard and response interface
5. **App.js** - React routing logic
### Important (Configuration & Styling)
6. **App.css** - All visual styling
7. **.env** - API keys configuration
8. **requirements.txt** - Backend dependencies
9. **package.json** - Frontend dependencies
### Reference (Documentation)
10. **SESSION_SUMMARY.md** - Understanding the architecture
11. **NEXT_STEPS.md** - Planning future work
12. **README.md** - Setup and feature overview
13. **QUICKSTART.md** - Fast setup instructions
---
## 🔍 Where to Find Things
### Adding New Features
**New WebSocket message type:**
- Backend: `main.py``character_websocket()` or `storyteller_websocket()`
- Frontend: Relevant component's `useEffect``ws.onmessage`
**New REST endpoint:**
- Backend: `main.py` → Add `@app.post()` or `@app.get()` decorator
- Frontend: Component → Add `fetch()` call
**New UI component:**
- Create in: `frontend/src/components/NewComponent.js`
- Import in: `frontend/src/App.js`
- Style in: `frontend/src/App.css`
**New LLM provider:**
- Backend: `main.py` → Update `call_llm()` function
- Frontend: `SessionSetup.js` → Model selector will auto-update from `/models` endpoint
### Understanding How It Works
**Message flow (Character → Storyteller):**
1. `CharacterView.js``sendMessage()` → WebSocket
2. `main.py``character_websocket()` → Receives message
3. Stored in `character.conversation_history`
4. Forwarded to storyteller via `manager.send_to_client()`
5. `StorytellerView.js``ws.onmessage` → Displays in UI
**Message flow (Storyteller → Character):**
1. `StorytellerView.js``sendResponse()` → WebSocket
2. `main.py``storyteller_websocket()` → Receives response
3. Stored in `character.conversation_history`
4. Sent to specific character via `manager.send_to_client()`
5. `CharacterView.js``ws.onmessage` → Displays in UI
**How LLM integration works:**
1. Character chooses model in `SessionSetup.js`
2. Stored in `Character.llm_model` field
3. When generating response: `call_llm(character.llm_model, messages)`
4. Function routes to OpenAI or OpenRouter based on model prefix
5. Returns generated text to be sent to character
### Styling
**Global styles:**
- `App.css` → Lines 1-100 (body, session-setup)
**SessionSetup styles:**
- `App.css` → Lines 18-180 (setup-container, input-group, divider, model-selector)
**CharacterView styles:**
- `App.css` → Lines 181-350 (character-view, character-header, messages)
**StorytellerView styles:**
- `App.css` → Lines 351-704 (storyteller-view, character-list, conversation-panel)
---
## 🛠️ Quick Modifications
### Change default LLM model
**File:** `main.py` line 47
```python
llm_model: str = "gpt-3.5-turbo" # Change default here
```
### Change ports
**Backend:** `main.py` line 397
```python
uvicorn.run(app, host="0.0.0.0", port=8000) # Change port here
```
**Frontend:** `package.json` or set `PORT=3001` environment variable
### Change API URLs
**File:** `SessionSetup.js`, `CharacterView.js`, `StorytellerView.js`
```javascript
const API_URL = 'http://localhost:8000'; // Change this
const WS_URL = 'ws://localhost:8000'; // And this
```
### Add new available model
**File:** `main.py``get_available_models()` function (lines 323-351)
```python
models["openai"].append({
"id": "gpt-4o-mini",
"name": "GPT-4o Mini",
"provider": "OpenAI"
})
```
### Modify color scheme
**File:** `App.css`
- Line 13: `background: linear-gradient(...)` - Main gradient
- Lines 100-150: Button colors (`.btn-primary`)
- Lines 400-450: Message colors (`.message.sent`, `.message.received`)
---
## 📊 File Statistics
| Category | Files | Total Lines |
|----------|-------|-------------|
| Backend | 2 | ~407 |
| Frontend JS | 5 | ~616 |
| Frontend CSS | 1 | 704 |
| Frontend HTML | 1 | 18 |
| Documentation | 5 | ~1,500 |
| Scripts | 3 | ~250 |
| **Total** | **17** | **~3,495** |
---
## 🔄 Git/Version Control
### Files that should be gitignored:
- `.env` (contains API keys)
- `node_modules/` (npm packages)
- `.venv/` or `venv/` (Python virtual environment)
- `__pycache__/` (Python cache)
- `frontend/build/` (React build output)
- `.DS_Store` (Mac system files)
### Files that should be committed:
- All `.js`, `.py`, `.css`, `.html` files
- All `.md` documentation files
- `.env.example` (template without real keys)
- `requirements.txt` and `package.json`
- All script files (`.sh`, `.bat`)
---
## 💡 Tips for Navigating
1. **Start with README.md** for overall understanding
2. **Check SESSION_SUMMARY.md** for architecture details
3. **Read main.py** to understand backend flow
4. **Look at App.js** to see component structure
5. **Each component is self-contained** - easy to modify independently
6. **Styles are organized by component** in App.css
7. **WebSocket messages use JSON** with a `type` field for routing
---
## 🆘 Troubleshooting Guide
### "Can't find file X"
- Check you're in project root: `/home/aodhan/projects/apps/storyteller`
- Frontend files are in `frontend/` subdirectory
- Components are in `frontend/src/components/`
### "Code isn't working"
- Check if both servers are running (backend on 8000, frontend on 3000)
- Check browser console for JavaScript errors
- Check terminal for Python errors
- Verify `.env` file exists with valid API keys
### "Want to understand feature X"
- Search in `SESSION_SUMMARY.md` for architecture
- Search in `main.py` for backend implementation
- Search in relevant component file for frontend
- Check `App.css` for styling
---
*Last Updated: October 11, 2025*
*Total Project Size: ~3,500 lines of code + documentation*

96
docs/setup/QUICKSTART.md Normal file
View File

@@ -0,0 +1,96 @@
# 🚀 Quick Start Guide
## Installation (5 minutes)
### 1. Backend Setup
```bash
# Install Python dependencies
pip install -r requirements.txt
# Create environment file (optional - only for AI suggestions)
cp .env.example .env
# Edit .env and add your OpenAI API key if desired
```
### 2. Frontend Setup
```bash
cd frontend
npm install
cd ..
```
## Running the App
### Terminal 1 - Backend
```bash
python main.py
# Server runs on http://localhost:8000
```
### Terminal 2 - Frontend
```bash
cd frontend
npm start
# Opens browser at http://localhost:3000
```
## First Session
### As Storyteller:
1. Open http://localhost:3000
2. Enter session name → "My Adventure"
3. Click "Create New Session"
4. **Copy the session ID** (shows at top of dashboard)
5. Share session ID with players
### As Player:
1. Open http://localhost:3000 (in different browser/tab)
2. Paste the session ID
3. Enter character details:
- Name: "Aragorn"
- Description: "A ranger from the north"
- Personality: "Brave and noble"
4. Click "Join Session"
5. Send private message to storyteller
### Storyteller Dashboard:
- See new character appear in left panel
- Click character name to view their private message
- Type response in bottom text area
- Click "Send Private Response"
- Use scene narration to broadcast to all characters
## Key Features
**Private Messaging**: Each character's conversation is completely isolated
**Real-time Updates**: WebSocket communication for instant delivery
**Scene Narration**: Broadcast story updates to all characters
**Pending Indicators**: Red badges show which characters need responses
**Conversation History**: Full chat history preserved per character
## Troubleshooting
**Backend won't start?**
- Check Python version: `python --version` (needs 3.8+)
- Install dependencies: `pip install -r requirements.txt`
**Frontend won't start?**
- Check Node version: `node --version` (needs 16+)
- Install dependencies: `cd frontend && npm install`
**WebSocket not connecting?**
- Ensure backend is running on port 8000
- Check browser console for errors
- Try refreshing the page
**Characters can't join?**
- Verify session ID is correct
- Ensure storyteller session is active
- Check that backend is running
## Next Steps
- Invite multiple players to test isolated conversations
- Try narrating scenes visible to all characters
- Experiment with rich character descriptions
- Optionally add OpenAI API key for AI-assisted storyteller suggestions

View File

@@ -0,0 +1,268 @@
# 🎯 Quick Reference - Storyteller RPG
## 📍 Where We Are Now
### ✅ Current Features (Working)
- Basic storyteller-character private messaging
- Real-time WebSocket communication
- Multiple LLM support (GPT-4, Claude, Llama, Gemini, etc.)
- Each character can use different AI model
- Session creation and joining
- Message history per character
- Scene narrations broadcast to all
### 📁 Current File Structure
```
windsurf-project/
├── main.py # Backend API (FastAPI + WebSockets)
├── requirements.txt # Python dependencies
├── .env # API keys configuration
├── start.sh # Startup script (Linux/Mac)
├── start.bat # Startup script (Windows)
├── frontend/
│ ├── src/
│ │ ├── App.js
│ │ ├── components/
│ │ │ ├── SessionSetup.js
│ │ │ ├── CharacterView.js
│ │ │ └── StorytellerView.js
│ │ └── App.css
│ ├── public/
│ │ └── index.html
│ └── package.json
├── README.md
├── LLM_GUIDE.md # Guide to available LLMs
├── PROJECT_PLAN.md # Full project roadmap
└── MVP_ROADMAP.md # MVP-specific plan
```
---
## 🎯 MVP Goals Summary
### Core User Modes
1. **Player** (Human/AI) - Play a character with profile
2. **Storyteller** (Human/AI) - Run the game, respond to players
3. **Gamemaster** (Human only) - Create/manage games, control AI
4. **Admin** (Dev only) - Full system access
### Key Features for MVP
1. **Public/Private Messages** - Players can have secret actions
2. **Character Profiles** - Race, class, gender, personality
3. **Import/Export** - Save characters as JSON or PNG with metadata
4. **AI Automation** - AI players and storytellers run automatically
5. **Game Management** - Create games, assign roles, save progress
6. **Permission System** - Each role has specific capabilities
---
## 🎭 Character Profile System (MVP)
### Races
- Human (Versatile)
- Elf (Graceful, perceptive)
- Dwarf (Sturdy, loyal)
- Orc (Strong, fierce)
- Halfling (Nimble, lucky)
### Classes
- Warrior (Combat)
- Wizard (Magic)
- Cleric (Healing)
- Archer (Ranged)
- Rogue (Stealth)
### Personalities
- Friendly (Optimistic)
- Serious (Focused)
- Doubtful (Cautious)
- Measured (Balanced)
### Storyteller Styles
- Narrator (3rd person)
- DM (2nd person)
- Internal Thoughts (1st person)
---
## 📋 Permission Matrix
| Action | Player | Storyteller | GM | Admin |
|--------|--------|-------------|-----|-------|
| View own messages | ✅ | ✅ | ✅ | ✅ |
| View public messages | ✅ | ✅ | ✅ | ✅ |
| View private messages | ❌ | ✅ | ✅ | ✅ |
| Edit own messages | ✅ | ✅ | ✅ | ✅ |
| Edit storyteller | ❌ | ✅ | ❌ | ✅ |
| Edit human players | ❌ | ❌ | ❌ | ✅ |
| Edit AI responses | ❌ | ✅ | ✅ | ✅ |
| Create games | ❌ | ❌ | ✅ | ✅ |
| Control AI | ❌ | ✅ | ✅ | ✅ |
---
## 🗺️ 12-Week Implementation Plan
### Weeks 1-2: Message System
- Public/private/mixed message types
- Enhanced message composer
- Visibility controls
### Weeks 3-4: Character Profiles
- Profile creation wizard
- Race/class/personality templates
- Import/export (JSON & PNG)
- Profile-based LLM prompts
### Weeks 5-7: User Modes
- Player interface
- Storyteller dashboard
- Gamemaster control panel
- Permission enforcement
### Weeks 8-9: AI Automation
- AI player system
- AI storyteller system
- Automation controls
- Override mechanisms
### Weeks 10-11: Game Management
- Game creation wizard
- Save/load system
- Database implementation
- Game checkpoints
### Week 12: Polish & Testing
- UI/UX refinement
- Testing suite
- Documentation
- Performance optimization
---
## 🔧 Technology Stack
### Current
- **Backend:** FastAPI, WebSockets, OpenAI/OpenRouter APIs
- **Frontend:** React, WebSocket client
- **Storage:** In-memory (temporary)
### Needed for MVP
- **Database:** SQLAlchemy + PostgreSQL/SQLite
- **Auth:** JWT tokens (python-jose)
- **Image:** Pillow (PNG metadata)
- **Frontend:** react-hook-form, zod, file-saver
---
## 🚀 How to Run (Current)
```bash
# Quick start (kills old instances, starts both servers)
./start.sh
# Manual start
# Terminal 1 - Backend
python main.py
# Terminal 2 - Frontend
cd frontend
npm start
```
**Access:** http://localhost:3000
---
## 📝 Development Notes
### Message Types (New Feature)
```
PUBLIC: "I shake the merchant's hand"
PRIVATE: "I attempt to pickpocket him"
MIXED: Both public and private actions in one message
```
### Character Profile Structure
```json
{
"name": "Thorin",
"gender": "Male",
"race": "Dwarf",
"class": "Warrior",
"personality": "Serious",
"llm_model": "anthropic/claude-3.5-sonnet",
"custom_prompts": {
"background": "A grizzled veteran...",
"response_style": "Speak gruffly and with honor"
}
}
```
### LLM Prompt Building
```python
# System prompt = Race + Class + Personality + Custom
"You are a Dwarf Warrior with a Serious personality.
You are stout and honorable. You excel in combat.
You are focused and pragmatic. Speak gruffly..."
```
---
## 🎯 Phase 1 Starting Point
**First Task:** Implement Public/Private Message System
**Steps:**
1. Update `Message` model in `main.py`:
```python
class Message(BaseModel):
visibility: str # "public", "private", "mixed"
public_content: Optional[str]
private_content: Optional[str]
```
2. Create message type selector in `CharacterView.js`:
```jsx
<select onChange={handleVisibilityChange}>
<option value="public">Public Action</option>
<option value="private">Private Action</option>
<option value="mixed">Both</option>
</select>
```
3. Update WebSocket message handling:
- Filter messages based on user role
- Storyteller sees all
- Players see only public messages from others
4. Update UI to show message type indicators
**Estimated Time:** 3-5 days
---
## 📚 Documentation Links
- **Full Roadmap:** `PROJECT_PLAN.md` (15 weeks, all phases)
- **MVP Plan:** `MVP_ROADMAP.md` (12 weeks, core features)
- **LLM Guide:** `LLM_GUIDE.md` (Available AI models)
- **README:** `README.md` (Setup and usage)
---
## ❓ Key Questions to Answer During Development
1. **Authentication:** JWT vs Session-based?
2. **Database:** PostgreSQL or MongoDB?
3. **Deployment:** Docker, cloud provider?
4. **AI Costs:** Budget limits per game/user?
5. **Scaling:** How many concurrent games?
6. **Context:** How to handle long game histories with LLM limits?
---
**Next Steps:** Review MVP_ROADMAP.md and begin Phase 1 implementation!
**Last Updated:** 2025-01-11
**Status:** Planning Complete → Ready for Development

39
frontend/package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "storyteller-frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"socket.io-client": "^4.7.2",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#667eea" />
<meta
name="description"
content="Storyteller RPG - A storyteller-centric roleplaying application"
/>
<title>Storyteller RPG</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1,8 @@
{
"short_name": "Storyteller RPG",
"name": "Storyteller RPG Application",
"start_url": ".",
"display": "standalone",
"theme_color": "#667eea",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

703
frontend/src/App.css Normal file
View File

@@ -0,0 +1,703 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
/* Session Setup */
.session-setup {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.setup-container {
background: white;
border-radius: 20px;
padding: 3rem;
max-width: 600px;
width: 100%;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.setup-container h1 {
font-size: 2.5rem;
color: #2d3748;
margin-bottom: 0.5rem;
text-align: center;
}
.subtitle {
text-align: center;
color: #718096;
margin-bottom: 2rem;
}
.setup-section {
margin-bottom: 2rem;
}
.setup-section h2 {
font-size: 1.5rem;
color: #2d3748;
margin-bottom: 0.5rem;
}
.section-description {
color: #718096;
font-size: 0.9rem;
margin-bottom: 1rem;
}
.input-group {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.input-group input,
.input-group textarea {
padding: 0.75rem 1rem;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
transition: all 0.3s;
}
.input-group input:focus,
.input-group textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.divider {
text-align: center;
margin: 2rem 0;
position: relative;
}
.divider::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: #e2e8f0;
}
.divider span {
background: white;
padding: 0 1rem;
position: relative;
color: #a0aec0;
font-weight: 600;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 0.75rem 2rem;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.model-selector {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.model-selector label {
font-weight: 600;
color: #2d3748;
font-size: 0.95rem;
}
.model-select {
padding: 0.75rem 1rem;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
background: white;
cursor: pointer;
transition: all 0.3s;
}
.model-select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.model-hint {
font-size: 0.85rem;
color: #718096;
font-style: italic;
margin: 0;
}
/* Character View */
.character-view {
min-height: 100vh;
background: white;
display: flex;
flex-direction: column;
}
.character-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.character-info h2 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.character-description {
opacity: 0.9;
margin-bottom: 0.25rem;
}
.character-personality {
opacity: 0.8;
font-style: italic;
}
.connection-status .status-indicator {
background: rgba(255, 255, 255, 0.2);
padding: 0.5rem 1rem;
border-radius: 20px;
font-size: 0.9rem;
}
.status-indicator.connected {
background: rgba(72, 187, 120, 0.3);
}
.current-scene {
background: #f7fafc;
padding: 1.5rem;
margin: 1rem;
border-left: 4px solid #667eea;
border-radius: 8px;
}
.current-scene h3 {
color: #2d3748;
margin-bottom: 0.5rem;
}
.current-scene p {
color: #4a5568;
line-height: 1.6;
}
.conversation-container {
flex: 1;
display: flex;
flex-direction: column;
padding: 1rem;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.empty-state {
text-align: center;
color: #a0aec0;
padding: 3rem;
}
.message {
max-width: 70%;
padding: 1rem;
border-radius: 12px;
animation: slideIn 0.3s ease-out;
padding: 0.75rem 1rem;
border-radius: 18px;
line-height: 1.4;
position: relative;
word-wrap: break-word;
}
.message.user {
align-self: flex-end;
background-color: #007bff;
color: white;
border-bottom-right-radius: 4px;
}
.message.other {
align-self: flex-start;
background-color: #f1f1f1;
color: #333;
border-bottom-left-radius: 4px;
}
.message-form {
display: flex;
padding: 1rem;
border-top: 1px solid #eee;
background-color: #f9f9f9;
}
.message-form input {
flex: 1;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
margin-right: 0.5rem;
}
.message-form button {
background-color: #2ecc71;
color: white;
border: none;
padding: 0 1.5rem;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.message-form button:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
.message-form button:hover:not(:disabled) {
background-color: #27ae60;
}
.storyteller-controls {
padding: 1rem;
background-color: #f8f9fa;
border-top: 1px solid #eee;
}
.storyteller-controls textarea {
width: 100%;
padding: 0.75rem;
margin-bottom: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
resize: vertical;
min-height: 60px;
}
.storyteller-controls button {
background-color: #9b59b6;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
width: 100%;
}
/* Storyteller View */
.storyteller-view {
min-height: 100vh;
background: #f7fafc;
}
.storyteller-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.storyteller-header h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.session-id {
opacity: 0.9;
margin: 0.5rem 0;
}
.session-id code {
background: rgba(255, 255, 255, 0.2);
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-family: 'Courier New', monospace;
}
.pending-badge {
background: #f56565;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 20px;
font-weight: 600;
animation: pulse 2s infinite;
}
.scene-section {
background: white;
margin: 1rem;
padding: 1.5rem;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.scene-section h3 {
color: #2d3748;
margin-bottom: 1rem;
}
.current-scene-display {
background: #f7fafc;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
color: #4a5568;
}
.scene-input {
display: flex;
gap: 1rem;
align-items: flex-end;
}
.scene-input textarea {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
font-family: inherit;
resize: vertical;
}
.scene-input textarea:focus {
outline: none;
border-color: #667eea;
}
.storyteller-content {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 1rem;
padding: 1rem;
min-height: calc(100vh - 400px);
}
.character-list {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
overflow-y: auto;
}
.character-list h3 {
color: #2d3748;
margin-bottom: 1rem;
}
.character-cards {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.character-card {
padding: 1rem;
border: 2px solid #e2e8f0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
}
.character-card:hover {
border-color: #667eea;
transform: translateX(4px);
}
.character-card.selected {
border-color: #667eea;
background: #f0f4ff;
}
.character-card.pending {
border-color: #f56565;
background: #fff5f5;
}
.character-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.character-card-header h4 {
color: #2d3748;
font-size: 1.1rem;
}
.pending-indicator {
color: #f56565;
font-size: 1.5rem;
animation: pulse 2s infinite;
}
.character-card-desc {
color: #718096;
font-size: 0.9rem;
margin-bottom: 0.25rem;
}
.character-card-personality {
color: #667eea;
font-size: 0.85rem;
font-style: italic;
margin-bottom: 0.25rem;
}
.character-card-model {
color: #48bb78;
font-size: 0.8rem;
font-family: 'Courier New', monospace;
margin-bottom: 0.25rem;
background: #f0fff4;
padding: 0.25rem 0.5rem;
border-radius: 4px;
display: inline-block;
}
.character-card-messages {
color: #a0aec0;
font-size: 0.8rem;
}
.conversation-panel {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
max-height: calc(100vh - 420px);
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e2e8f0;
}
.panel-header h3 {
color: #2d3748;
}
.pending-label {
background: #f56565;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 12px;
font-size: 0.85rem;
font-weight: 600;
}
.conversation-messages {
flex: 1;
overflow-y: auto;
padding: 1rem 0;
display: flex;
flex-direction: column;
gap: 1rem;
}
.message.from-character {
align-self: flex-start;
background: #f0f4ff;
border: 2px solid #667eea;
max-width: 70%;
padding: 1rem;
border-radius: 12px;
}
.message.from-storyteller {
align-self: flex-end;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
max-width: 70%;
padding: 1rem;
border-radius: 12px;
}
.response-section {
margin-top: 1rem;
padding-top: 1rem;
border-top: 2px solid #e2e8f0;
}
.response-section h4 {
color: #2d3748;
margin-bottom: 0.75rem;
}
.response-section textarea {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
font-family: inherit;
resize: vertical;
margin-bottom: 0.75rem;
}
.response-section textarea:focus {
outline: none;
border-color: #667eea;
}
.message.sent {
align-self: flex-end;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.message.received {
align-self: flex-start;
background: #f7fafc;
border: 2px solid #e2e8f0;
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.85rem;
opacity: 0.8;
}
.message-content {
line-height: 1.5;
}
.message-form {
display: flex;
gap: 0.75rem;
padding: 1rem;
background: #f7fafc;
border-top: 2px solid #e2e8f0;
}
.message-form input {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 1rem;
}
.message-form input:focus {
outline: none;
border-color: #667eea;
}
.message-form button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 0.75rem 2rem;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.message-form button:hover:not(:disabled) {
transform: translateY(-2px);
}
.message-form button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@media (max-width: 768px) {
.storyteller-content {
grid-template-columns: 1fr;
}
.character-list {
max-height: 300px;
}
.message {
max-width: 85%;
}
}

39
frontend/src/App.js Normal file
View File

@@ -0,0 +1,39 @@
import React, { useState } from 'react';
import './App.css';
import SessionSetup from './components/SessionSetup';
import StorytellerView from './components/StorytellerView';
import CharacterView from './components/CharacterView';
function App() {
const [sessionId, setSessionId] = useState('');
const [characterId, setCharacterId] = useState('');
const [isStoryteller, setIsStoryteller] = useState(false);
const handleCreateSession = (sid) => {
setSessionId(sid);
setIsStoryteller(true);
};
const handleJoinSession = (sid, cid) => {
setSessionId(sid);
setCharacterId(cid);
setIsStoryteller(false);
};
if (!sessionId) {
return (
<SessionSetup
onCreateSession={handleCreateSession}
onJoinSession={handleJoinSession}
/>
);
}
if (isStoryteller) {
return <StorytellerView sessionId={sessionId} />;
}
return <CharacterView sessionId={sessionId} characterId={characterId} />;
}
export default App;

View File

@@ -0,0 +1,140 @@
import React, { useState, useEffect, useRef } from 'react';
const API_URL = 'http://localhost:8000';
const WS_URL = 'ws://localhost:8000';
function CharacterView({ sessionId, characterId }) {
const [messages, setMessages] = useState([]);
const [inputMessage, setInputMessage] = useState('');
const [isConnected, setIsConnected] = useState(false);
const [characterInfo, setCharacterInfo] = useState(null);
const [currentScene, setCurrentScene] = useState('');
const wsRef = useRef(null);
const messagesEndRef = useRef(null);
useEffect(() => {
// Fetch character info
fetch(`${API_URL}/sessions/${sessionId}/characters/${characterId}/conversation`)
.then(res => res.json())
.then(data => {
setCharacterInfo(data.character);
setMessages(data.conversation || []);
})
.catch(err => console.error('Error fetching character:', err));
// Connect to WebSocket
const ws = new WebSocket(`${WS_URL}/ws/character/${sessionId}/${characterId}`);
ws.onopen = () => {
console.log('Connected to WebSocket');
setIsConnected(true);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'history') {
setMessages(data.messages || []);
} else if (data.type === 'storyteller_response') {
setMessages(prev => [...prev, data.message]);
} else if (data.type === 'scene_narration') {
setCurrentScene(data.content);
}
};
ws.onclose = () => {
console.log('Disconnected from WebSocket');
setIsConnected(false);
};
wsRef.current = ws;
return () => {
ws.close();
};
}, [sessionId, characterId]);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const sendMessage = (e) => {
e.preventDefault();
if (!inputMessage.trim() || !isConnected) return;
const message = {
type: 'message',
content: inputMessage
};
wsRef.current.send(JSON.stringify(message));
setMessages(prev => [...prev, { sender: 'character', content: inputMessage, timestamp: new Date().toISOString() }]);
setInputMessage('');
};
return (
<div className="character-view">
<div className="character-header">
<div className="character-info">
<h2>{characterInfo?.name || 'Loading...'}</h2>
<p className="character-description">{characterInfo?.description}</p>
{characterInfo?.personality && (
<p className="character-personality">🎭 {characterInfo.personality}</p>
)}
</div>
<div className="connection-status">
<span className={`status-indicator ${isConnected ? 'connected' : 'disconnected'}`}>
{isConnected ? '● Connected' : '○ Disconnected'}
</span>
</div>
</div>
{currentScene && (
<div className="current-scene">
<h3>📜 Current Scene</h3>
<p>{currentScene}</p>
</div>
)}
<div className="conversation-container">
<div className="messages">
{messages.length === 0 ? (
<div className="empty-state">
<p>No messages yet. Send a message to the storyteller to begin!</p>
</div>
) : (
messages.map((msg, index) => (
<div key={index} className={`message ${msg.sender === 'character' ? 'sent' : 'received'}`}>
<div className="message-header">
<span className="message-sender">
{msg.sender === 'character' ? characterInfo?.name : '🎲 Storyteller'}
</span>
<span className="message-time">
{new Date(msg.timestamp).toLocaleTimeString()}
</span>
</div>
<div className="message-content">{msg.content}</div>
</div>
))
)}
<div ref={messagesEndRef} />
</div>
<form onSubmit={sendMessage} className="message-form">
<input
type="text"
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
placeholder="Send a private message to the storyteller..."
disabled={!isConnected}
/>
<button type="submit" disabled={!isConnected}>
Send
</button>
</form>
</div>
</div>
);
}
export default CharacterView;

View File

@@ -0,0 +1,179 @@
import React, { useState, useEffect } from 'react';
const API_URL = 'http://localhost:8000';
function SessionSetup({ onCreateSession, onJoinSession }) {
const [sessionName, setSessionName] = useState('');
const [joinSessionId, setJoinSessionId] = useState('');
const [characterName, setCharacterName] = useState('');
const [characterDesc, setCharacterDesc] = useState('');
const [characterPersonality, setCharacterPersonality] = useState('');
const [selectedModel, setSelectedModel] = useState('gpt-3.5-turbo');
const [availableModels, setAvailableModels] = useState({ openai: [], openrouter: [] });
useEffect(() => {
// Fetch available models
fetch(`${API_URL}/models`)
.then(res => res.json())
.then(data => {
setAvailableModels(data);
// Set default model based on what's available
if (data.openai.length > 0) {
setSelectedModel(data.openai[0].id);
} else if (data.openrouter.length > 0) {
setSelectedModel(data.openrouter[0].id);
}
})
.catch(err => console.error('Error fetching models:', err));
}, []);
const createSession = async () => {
if (!sessionName.trim()) {
alert('Please enter a session name');
return;
}
try {
const params = new URLSearchParams({ name: sessionName });
const response = await fetch(`${API_URL}/sessions/?${params}`, {
method: 'POST',
});
const data = await response.json();
onCreateSession(data.id);
} catch (error) {
console.error('Error creating session:', error);
alert('Failed to create session');
}
};
const joinSession = async () => {
if (!joinSessionId.trim() || !characterName.trim() || !characterDesc.trim()) {
alert('Please fill in all required fields');
return;
}
try {
const response = await fetch(`${API_URL}/sessions/${joinSessionId}`);
if (!response.ok) {
alert('Session not found');
return;
}
const params = new URLSearchParams({
name: characterName,
description: characterDesc,
personality: characterPersonality,
llm_model: selectedModel,
});
const charResponse = await fetch(`${API_URL}/sessions/${joinSessionId}/characters/?${params}`, {
method: 'POST',
});
const charData = await charResponse.json();
onJoinSession(joinSessionId, charData.id);
} catch (error) {
console.error('Error joining session:', error);
alert('Failed to join session');
}
};
return (
<div className="session-setup">
<div className="setup-container">
<h1>🎭 Storyteller RPG</h1>
<p className="subtitle">Private character-storyteller interactions</p>
<div className="setup-section">
<h2>Create New Session</h2>
<p className="section-description">Start a new game as the storyteller</p>
<div className="input-group">
<input
type="text"
placeholder="Enter session name"
value={sessionName}
onChange={(e) => setSessionName(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && createSession()}
/>
<button className="btn-primary" onClick={createSession}>
Create Session
</button>
</div>
</div>
<div className="divider">
<span>OR</span>
</div>
<div className="setup-section">
<h2>Join Existing Session</h2>
<p className="section-description">Play as a character in an ongoing game</p>
<div className="input-group">
<input
type="text"
placeholder="Session ID"
value={joinSessionId}
onChange={(e) => setJoinSessionId(e.target.value)}
/>
<input
type="text"
placeholder="Character Name *"
value={characterName}
onChange={(e) => setCharacterName(e.target.value)}
/>
<textarea
placeholder="Character Description (e.g., A brave knight seeking redemption) *"
value={characterDesc}
onChange={(e) => setCharacterDesc(e.target.value)}
rows="3"
/>
<textarea
placeholder="Personality Traits (optional)"
value={characterPersonality}
onChange={(e) => setCharacterPersonality(e.target.value)}
rows="2"
/>
<div className="model-selector">
<label>🤖 Character AI Model</label>
<select
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
className="model-select"
>
{availableModels.openai.length > 0 && (
<optgroup label="OpenAI Models">
{availableModels.openai.map(model => (
<option key={model.id} value={model.id}>
{model.name}
</option>
))}
</optgroup>
)}
{availableModels.openrouter.length > 0 && (
<optgroup label="OpenRouter Models">
{availableModels.openrouter.map(model => (
<option key={model.id} value={model.id}>
{model.name} ({model.provider})
</option>
))}
</optgroup>
)}
</select>
<p className="model-hint">
Different models give different personalities! Try Claude for creative responses,
GPT-4 for detailed reasoning, or Llama for faster interactions.
</p>
</div>
<button className="btn-primary" onClick={joinSession}>
Join Session
</button>
</div>
</div>
</div>
</div>
);
}
export default SessionSetup;

View File

@@ -0,0 +1,242 @@
import React, { useState, useEffect, useRef } from 'react';
const API_URL = 'http://localhost:8000';
const WS_URL = 'ws://localhost:8000';
function StorytellerView({ sessionId }) {
const [characters, setCharacters] = useState({});
const [selectedCharacter, setSelectedCharacter] = useState(null);
const [responseText, setResponseText] = useState('');
const [sceneText, setSceneText] = useState('');
const [currentScene, setCurrentScene] = useState('');
const [isConnected, setIsConnected] = useState(false);
const wsRef = useRef(null);
useEffect(() => {
// Connect to WebSocket
const ws = new WebSocket(`${WS_URL}/ws/storyteller/${sessionId}`);
ws.onopen = () => {
console.log('Storyteller connected to WebSocket');
setIsConnected(true);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'session_state') {
setCharacters(data.characters || {});
setCurrentScene(data.current_scene || '');
} else if (data.type === 'character_message') {
// Update character with new message
setCharacters(prev => ({
...prev,
[data.character_id]: {
...prev[data.character_id],
conversation_history: [
...(prev[data.character_id]?.conversation_history || []),
data.message
],
pending_response: true
}
}));
} else if (data.type === 'character_joined') {
// Refresh character list
fetch(`${API_URL}/sessions/${sessionId}`)
.then(res => res.json())
.then(session => {
const charMap = {};
Object.entries(session.characters).forEach(([id, char]) => {
charMap[id] = {
...char,
conversation_history: char.conversation_history || [],
pending_response: char.pending_response || false
};
});
setCharacters(charMap);
});
}
};
ws.onclose = () => {
console.log('Storyteller disconnected from WebSocket');
setIsConnected(false);
};
wsRef.current = ws;
return () => {
ws.close();
};
}, [sessionId]);
const sendResponse = () => {
if (!selectedCharacter || !responseText.trim() || !isConnected) return;
const message = {
type: 'respond_to_character',
character_id: selectedCharacter,
content: responseText
};
wsRef.current.send(JSON.stringify(message));
// Update local state
setCharacters(prev => ({
...prev,
[selectedCharacter]: {
...prev[selectedCharacter],
conversation_history: [
...(prev[selectedCharacter]?.conversation_history || []),
{ sender: 'storyteller', content: responseText, timestamp: new Date().toISOString() }
],
pending_response: false
}
}));
setResponseText('');
};
const narrateScene = () => {
if (!sceneText.trim() || !isConnected) return;
const message = {
type: 'narrate_scene',
content: sceneText
};
wsRef.current.send(JSON.stringify(message));
setCurrentScene(sceneText);
setSceneText('');
};
const selectedChar = selectedCharacter ? characters[selectedCharacter] : null;
const pendingCount = Object.values(characters).filter(c => c.pending_response).length;
return (
<div className="storyteller-view">
<div className="storyteller-header">
<div>
<h1>🎲 Storyteller Dashboard</h1>
<p className="session-id">Session ID: <code>{sessionId}</code></p>
<p className="connection-status">
<span className={`status-indicator ${isConnected ? 'connected' : 'disconnected'}`}>
{isConnected ? '● Connected' : '○ Disconnected'}
</span>
</p>
</div>
{pendingCount > 0 && (
<div className="pending-badge">
{pendingCount} pending response{pendingCount !== 1 ? 's' : ''}
</div>
)}
</div>
<div className="scene-section">
<h3>📜 Narrate Scene to All Characters</h3>
{currentScene && (
<div className="current-scene-display">
<strong>Current Scene:</strong> {currentScene}
</div>
)}
<div className="scene-input">
<textarea
placeholder="Describe the scene that all characters will experience..."
value={sceneText}
onChange={(e) => setSceneText(e.target.value)}
rows="3"
/>
<button className="btn-primary" onClick={narrateScene} disabled={!isConnected}>
Narrate Scene
</button>
</div>
</div>
<div className="storyteller-content">
<div className="character-list">
<h3>Characters ({Object.keys(characters).length})</h3>
{Object.keys(characters).length === 0 ? (
<div className="empty-state">
<p>No characters yet. Share the session ID for players to join!</p>
</div>
) : (
<div className="character-cards">
{Object.entries(characters).map(([id, char]) => (
<div
key={id}
className={`character-card ${selectedCharacter === id ? 'selected' : ''} ${char.pending_response ? 'pending' : ''}`}
onClick={() => setSelectedCharacter(id)}
>
<div className="character-card-header">
<h4>{char.name}</h4>
{char.pending_response && <span className="pending-indicator"></span>}
</div>
<p className="character-card-desc">{char.description}</p>
{char.personality && <p className="character-card-personality">🎭 {char.personality}</p>}
{char.llm_model && <p className="character-card-model">🤖 {char.llm_model}</p>}
<p className="character-card-messages">
{char.conversation_history?.length || 0} message{char.conversation_history?.length !== 1 ? 's' : ''}
</p>
</div>
))}
</div>
)}
</div>
<div className="conversation-panel">
{selectedChar ? (
<>
<div className="panel-header">
<h3>Conversation with {selectedChar.name}</h3>
{selectedChar.pending_response && (
<span className="pending-label">Awaiting Response</span>
)}
</div>
<div className="conversation-messages">
{selectedChar.conversation_history?.length === 0 ? (
<div className="empty-state">
<p>No conversation yet with this character.</p>
</div>
) : (
selectedChar.conversation_history?.map((msg, index) => (
<div key={index} className={`message ${msg.sender === 'character' ? 'from-character' : 'from-storyteller'}`}>
<div className="message-header">
<span className="message-sender">
{msg.sender === 'character' ? selectedChar.name : 'You (Storyteller)'}
</span>
<span className="message-time">
{new Date(msg.timestamp).toLocaleTimeString()}
</span>
</div>
<div className="message-content">{msg.content}</div>
</div>
))
)}
</div>
<div className="response-section">
<h4>Respond to {selectedChar.name}</h4>
<textarea
placeholder={`Craft your response to ${selectedChar.name}. This is private and only they will see it.`}
value={responseText}
onChange={(e) => setResponseText(e.target.value)}
rows="4"
/>
<button className="btn-primary" onClick={sendResponse} disabled={!isConnected}>
Send Private Response
</button>
</div>
</>
) : (
<div className="empty-state">
<p>Select a character to view their conversation and respond</p>
</div>
)}
</div>
</div>
</div>
);
}
export default StorytellerView;

11
frontend/src/index.js Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './App.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

397
main.py Normal file
View File

@@ -0,0 +1,397 @@
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from typing import Dict, List, Optional
import uuid
import os
from dotenv import load_dotenv
from openai import AsyncOpenAI
import asyncio
from datetime import datetime
import httpx
# Load environment variables
load_dotenv()
# Initialize FastAPI
app = FastAPI(title="Storyteller RPG API")
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Initialize OpenAI
client = AsyncOpenAI(api_key=os.getenv("OPENAI_API_KEY"))
openrouter_api_key = os.getenv("OPENROUTER_API_KEY")
if not os.getenv("OPENAI_API_KEY") and not openrouter_api_key:
print("Warning: Neither OPENAI_API_KEY nor OPENROUTER_API_KEY set. AI features will not work.")
# Models
class Message(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
sender: str # "character" or "storyteller"
content: str
timestamp: str = Field(default_factory=lambda: datetime.now().isoformat())
class Character(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
name: str
description: str
personality: str = "" # Additional personality traits
llm_model: str = "gpt-3.5-turbo" # LLM model for this character
conversation_history: List[Message] = [] # Private conversation with storyteller
pending_response: bool = False # Waiting for storyteller response
class StorytellerResponse(BaseModel):
character_id: str
content: str
class GameSession(BaseModel):
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
name: str
characters: Dict[str, Character] = {}
current_scene: str = ""
scene_history: List[str] = [] # All scenes narrated
# In-memory storage (replace with database in production)
sessions: Dict[str, GameSession] = {}
# WebSocket connection manager
class ConnectionManager:
def __init__(self):
self.active_connections: Dict[str, WebSocket] = {} # key: "session_character" or "session_storyteller"
async def connect(self, websocket: WebSocket, client_id: str):
await websocket.accept()
self.active_connections[client_id] = websocket
def disconnect(self, client_id: str):
if client_id in self.active_connections:
del self.active_connections[client_id]
async def send_to_client(self, client_id: str, message: dict):
if client_id in self.active_connections:
await self.active_connections[client_id].send_json(message)
manager = ConnectionManager()
# API Endpoints
@app.post("/sessions/")
async def create_session(name: str):
session = GameSession(name=name)
sessions[session.id] = session
return session
@app.get("/sessions/{session_id}")
async def get_session(session_id: str):
if session_id not in sessions:
raise HTTPException(status_code=404, detail="Session not found")
return sessions[session_id]
@app.post("/sessions/{session_id}/characters/")
async def add_character(
session_id: str,
name: str,
description: str,
personality: str = "",
llm_model: str = "gpt-3.5-turbo"
):
if session_id not in sessions:
raise HTTPException(status_code=404, detail="Session not found")
character = Character(
name=name,
description=description,
personality=personality,
llm_model=llm_model
)
session = sessions[session_id]
session.characters[character.id] = character
# Notify storyteller of new character
storyteller_key = f"{session_id}_storyteller"
if storyteller_key in manager.active_connections:
await manager.send_to_client(storyteller_key, {
"type": "character_joined",
"character": {
"id": character.id,
"name": character.name,
"description": character.description,
"llm_model": character.llm_model
}
})
return character
# WebSocket endpoint for character interactions (character view)
@app.websocket("/ws/character/{session_id}/{character_id}")
async def character_websocket(websocket: WebSocket, session_id: str, character_id: str):
if session_id not in sessions or character_id not in sessions[session_id].characters:
await websocket.close(code=1008, reason="Session or character not found")
return
client_key = f"{session_id}_{character_id}"
await manager.connect(websocket, client_key)
try:
# Send conversation history
session = sessions[session_id]
character = session.characters[character_id]
await websocket.send_json({
"type": "history",
"messages": [msg.dict() for msg in character.conversation_history]
})
while True:
data = await websocket.receive_json()
if data.get("type") == "message":
# Character sends message to storyteller
message = Message(sender="character", content=data["content"])
character.conversation_history.append(message)
character.pending_response = True
# Forward to storyteller
storyteller_key = f"{session_id}_storyteller"
if storyteller_key in manager.active_connections:
await manager.send_to_client(storyteller_key, {
"type": "character_message",
"character_id": character_id,
"character_name": character.name,
"message": message.dict()
})
except WebSocketDisconnect:
manager.disconnect(client_key)
# WebSocket endpoint for storyteller
@app.websocket("/ws/storyteller/{session_id}")
async def storyteller_websocket(websocket: WebSocket, session_id: str):
if session_id not in sessions:
await websocket.close(code=1008, reason="Session not found")
return
client_key = f"{session_id}_storyteller"
await manager.connect(websocket, client_key)
try:
# Send all characters and their conversation states
session = sessions[session_id]
await websocket.send_json({
"type": "session_state",
"characters": {
char_id: {
"id": char.id,
"name": char.name,
"description": char.description,
"personality": char.personality,
"conversation_history": [msg.dict() for msg in char.conversation_history],
"pending_response": char.pending_response
}
for char_id, char in session.characters.items()
},
"current_scene": session.current_scene
})
while True:
data = await websocket.receive_json()
if data.get("type") == "respond_to_character":
# Storyteller responds to a specific character
character_id = data["character_id"]
content = data["content"]
if character_id in session.characters:
character = session.characters[character_id]
message = Message(sender="storyteller", content=content)
character.conversation_history.append(message)
character.pending_response = False
# Send to character
char_key = f"{session_id}_{character_id}"
if char_key in manager.active_connections:
await manager.send_to_client(char_key, {
"type": "storyteller_response",
"message": message.dict()
})
elif data.get("type") == "narrate_scene":
# Broadcast scene to all characters
scene = data["content"]
session.current_scene = scene
session.scene_history.append(scene)
# Send to all connected characters
for char_id in session.characters:
char_key = f"{session_id}_{char_id}"
if char_key in manager.active_connections:
await manager.send_to_client(char_key, {
"type": "scene_narration",
"content": scene
})
except WebSocketDisconnect:
manager.disconnect(client_key)
# AI-assisted response generation using character's specific LLM
async def call_llm(model: str, messages: List[dict], temperature: float = 0.8, max_tokens: int = 200) -> str:
"""Call LLM via OpenRouter or OpenAI depending on model"""
# OpenAI models
if model.startswith("gpt-") or model.startswith("o1-"):
if not os.getenv("OPENAI_API_KEY"):
return "OpenAI API key not set."
try:
response = await client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens
)
return response.choices[0].message.content
except Exception as e:
return f"OpenAI error: {str(e)}"
# OpenRouter models (Claude, Llama, Gemini, etc.)
else:
if not openrouter_api_key:
return "OpenRouter API key not set."
try:
async with httpx.AsyncClient() as http_client:
response = await http_client.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={
"Authorization": f"Bearer {openrouter_api_key}",
"HTTP-Referer": "http://localhost:3000",
"X-Title": "Storyteller RPG"
},
json={
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens
},
timeout=30.0
)
response.raise_for_status()
data = response.json()
return data["choices"][0]["message"]["content"]
except Exception as e:
return f"OpenRouter error: {str(e)}"
@app.post("/sessions/{session_id}/generate_suggestion")
async def generate_suggestion(session_id: str, character_id: str, context: str = ""):
"""Generate AI suggestion for storyteller response to a character using the character's LLM"""
if session_id not in sessions:
raise HTTPException(status_code=404, detail="Session not found")
session = sessions[session_id]
if character_id not in session.characters:
raise HTTPException(status_code=404, detail="Character not found")
character = session.characters[character_id]
# Prepare context for AI suggestion
messages = [
{
"role": "system",
"content": f"You are {character.name} in an RPG. Respond in character. Character description: {character.description}. Personality: {character.personality}. Current scene: {session.current_scene}"
}
]
# Add recent conversation history
for msg in character.conversation_history[-6:]:
role = "assistant" if msg.sender == "character" else "user"
messages.append({"role": role, "content": msg.content})
if context:
messages.append({"role": "user", "content": f"Additional context: {context}"})
try:
suggestion = await call_llm(character.llm_model, messages, temperature=0.8, max_tokens=200)
return {"suggestion": suggestion, "model_used": character.llm_model}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error generating suggestion: {str(e)}")
# Get available LLM models
@app.get("/models")
async def get_available_models():
"""Get list of available LLM models"""
models = {
"openai": [],
"openrouter": []
}
if os.getenv("OPENAI_API_KEY"):
models["openai"] = [
{"id": "gpt-4o", "name": "GPT-4o (Latest)", "provider": "OpenAI"},
{"id": "gpt-4-turbo", "name": "GPT-4 Turbo", "provider": "OpenAI"},
{"id": "gpt-4", "name": "GPT-4", "provider": "OpenAI"},
{"id": "gpt-3.5-turbo", "name": "GPT-3.5 Turbo (Fast & Cheap)", "provider": "OpenAI"},
]
if openrouter_api_key:
models["openrouter"] = [
{"id": "anthropic/claude-3.5-sonnet", "name": "Claude 3.5 Sonnet", "provider": "Anthropic"},
{"id": "anthropic/claude-3-opus", "name": "Claude 3 Opus", "provider": "Anthropic"},
{"id": "anthropic/claude-3-haiku", "name": "Claude 3 Haiku (Fast)", "provider": "Anthropic"},
{"id": "google/gemini-pro-1.5", "name": "Gemini Pro 1.5", "provider": "Google"},
{"id": "meta-llama/llama-3.1-70b-instruct", "name": "Llama 3.1 70B", "provider": "Meta"},
{"id": "meta-llama/llama-3.1-8b-instruct", "name": "Llama 3.1 8B (Fast)", "provider": "Meta"},
{"id": "mistralai/mistral-large", "name": "Mistral Large", "provider": "Mistral"},
{"id": "cohere/command-r-plus", "name": "Command R+", "provider": "Cohere"},
]
return models
# Get all pending character messages
@app.get("/sessions/{session_id}/pending_messages")
async def get_pending_messages(session_id: str):
if session_id not in sessions:
raise HTTPException(status_code=404, detail="Session not found")
session = sessions[session_id]
pending = {}
for char_id, char in session.characters.items():
if char.pending_response:
last_message = char.conversation_history[-1] if char.conversation_history else None
if last_message and last_message.sender == "character":
pending[char_id] = {
"character_name": char.name,
"message": last_message.dict()
}
return pending
# Get character conversation history (for storyteller)
@app.get("/sessions/{session_id}/characters/{character_id}/conversation")
async def get_character_conversation(session_id: str, character_id: str):
if session_id not in sessions:
raise HTTPException(status_code=404, detail="Session not found")
session = sessions[session_id]
if character_id not in session.characters:
raise HTTPException(status_code=404, detail="Character not found")
character = session.characters[character_id]
return {
"character": {
"id": character.id,
"name": character.name,
"description": character.description,
"personality": character.personality
},
"conversation": [msg.dict() for msg in character.conversation_history],
"pending_response": character.pending_response
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

8
requirements.txt Normal file
View File

@@ -0,0 +1,8 @@
fastapi==0.104.1
uvicorn==0.24.0
python-dotenv==1.0.0
openai==1.3.0
python-multipart==0.0.6
pydantic==2.4.2
httpx==0.25.0
websockets==12.0

87
start.bat Normal file
View File

@@ -0,0 +1,87 @@
@echo off
REM Storyteller RPG Startup Script for Windows
REM This script starts both the backend and frontend servers
echo.
echo 🎭 Starting Storyteller RPG...
echo.
REM Kill any existing instances
echo Checking for existing instances...
REM Kill processes on port 8000 (backend)
for /f "tokens=5" %%a in ('netstat -aon ^| find ":8000" ^| find "LISTENING"') do (
echo Killing existing backend on port 8000 (PID: %%a)
taskkill /F /PID %%a >nul 2>&1
)
REM Kill processes on port 3000 (frontend)
for /f "tokens=5" %%a in ('netstat -aon ^| find ":3000" ^| find "LISTENING"') do (
echo Killing existing frontend on port 3000 (PID: %%a)
taskkill /F /PID %%a >nul 2>&1
)
REM Wait for ports to be released
timeout /t 2 /nobreak >nul
echo Ready to start fresh instances
echo.
REM Check if virtual environment exists
if not exist ".venv" (
echo Creating virtual environment...
python -m venv .venv
)
REM Activate virtual environment
echo Activating virtual environment...
call .venv\Scripts\activate.bat
REM Install backend dependencies if needed
python -c "import fastapi" 2>nul
if errorlevel 1 (
echo Installing backend dependencies...
pip install -r requirements.txt
)
REM Check if frontend dependencies are installed
if not exist "frontend\node_modules" (
echo Installing frontend dependencies...
cd frontend
call npm install
cd ..
)
echo.
echo Starting Backend (http://localhost:8000)...
start "Storyteller Backend" cmd /k "python main.py"
REM Wait a moment for backend to start
timeout /t 3 /nobreak >nul
echo Starting Frontend (http://localhost:3000)...
cd frontend
start "Storyteller Frontend" cmd /k "npm start"
cd ..
echo.
echo ✅ Servers are starting!
echo Backend: http://localhost:8000
echo Frontend: http://localhost:3000
echo.
REM Wait for frontend to be ready, then open browser
echo Waiting for frontend to start...
:wait_loop
timeout /t 1 /nobreak >nul
curl -s http://localhost:3000 >nul 2>&1
if errorlevel 1 goto wait_loop
echo Frontend ready! Opening browser...
start http://localhost:3000
echo.
echo Both servers are running in separate windows.
echo Close those windows to stop the servers.
echo.
pause

130
start.sh Executable file
View File

@@ -0,0 +1,130 @@
#!/bin/bash
# Storyteller RPG Startup Script
# This script starts both the backend and frontend servers
echo "🎭 Starting Storyteller RPG..."
echo ""
# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[0;33m'
NC='\033[0m' # No Color
# Kill any existing instances
echo -e "${YELLOW}Checking for existing instances...${NC}"
# Kill any process using port 8000 (backend)
BACKEND_PORT_PID=$(lsof -ti:8000)
if [ ! -z "$BACKEND_PORT_PID" ]; then
echo " Killing existing backend on port 8000 (PID: $BACKEND_PORT_PID)"
kill -9 $BACKEND_PORT_PID 2>/dev/null
fi
# Kill any process using port 3000 (frontend)
FRONTEND_PORT_PID=$(lsof -ti:3000)
if [ ! -z "$FRONTEND_PORT_PID" ]; then
echo " Killing existing frontend on port 3000 (PID: $FRONTEND_PORT_PID)"
kill -9 $FRONTEND_PORT_PID 2>/dev/null
fi
# Kill any existing node/python processes from this project
pkill -f "main.py" 2>/dev/null
pkill -f "react-scripts start" 2>/dev/null
# Wait a moment for ports to be released
sleep 1
echo -e "${GREEN}✓ Ready to start fresh instances${NC}"
echo ""
# Check if virtual environment exists
if [ ! -d ".venv" ]; then
echo "Creating virtual environment..."
python3 -m venv .venv
fi
# Activate virtual environment
echo -e "${BLUE}Activating virtual environment...${NC}"
source .venv/bin/activate
# Install backend dependencies if needed
if ! .venv/bin/python -c "import fastapi" 2>/dev/null; then
echo -e "${BLUE}Installing backend dependencies...${NC}"
.venv/bin/pip install -r requirements.txt
fi
# Check if frontend dependencies are installed
if [ ! -d "frontend/node_modules" ]; then
echo -e "${BLUE}Installing frontend dependencies...${NC}"
cd frontend
npm install
cd ..
fi
# Function to cleanup on exit
cleanup() {
echo ""
echo "Shutting down servers..."
kill $BACKEND_PID $FRONTEND_PID 2>/dev/null
exit 0
}
trap cleanup SIGINT SIGTERM
# Start backend
echo -e "${GREEN}Starting Backend (http://localhost:8000)...${NC}"
.venv/bin/python main.py &
BACKEND_PID=$!
# Wait a moment for backend to start
sleep 2
# Start frontend
echo -e "${GREEN}Starting Frontend (http://localhost:3000)...${NC}"
cd frontend
npm start &
FRONTEND_PID=$!
cd ..
echo ""
echo "✅ Servers are starting!"
echo " Backend: http://localhost:8000"
echo " Frontend: http://localhost:3000"
echo ""
echo "Press Ctrl+C to stop both servers"
echo ""
# Wait for frontend to be ready, then open browser
echo "Waiting for frontend to start..."
for i in {1..30}; do
if curl -s http://localhost:3000 > /dev/null 2>&1; then
echo -e "${GREEN}✓ Frontend ready!${NC}"
sleep 1
# Open browser based on OS
if command -v xdg-open > /dev/null; then
echo "Opening browser..."
xdg-open http://localhost:3000 > /dev/null 2>&1 &
elif command -v open > /dev/null; then
echo "Opening browser..."
open http://localhost:3000 > /dev/null 2>&1 &
elif command -v firefox > /dev/null; then
echo "Opening browser..."
firefox http://localhost:3000 > /dev/null 2>&1 &
elif command -v google-chrome > /dev/null; then
echo "Opening browser..."
google-chrome http://localhost:3000 > /dev/null 2>&1 &
else
echo "Please open your browser to: http://localhost:3000"
fi
break
fi
sleep 1
done
echo ""
# Wait for both processes
wait $BACKEND_PID $FRONTEND_PID