Upload 15 files
Browse files- DEPLOYMENT.md +318 -0
- Procfile +1 -0
- QUICKSTART.md +52 -0
- README.md +182 -14
- README_HF.md +116 -0
- app.py +8 -0
- app_gradio.py +296 -0
- application.py +201 -0
- codecommit-policy.json +63 -0
- data/guest_list.csv +122 -0
- requirements.txt +4 -0
- requirements_hf.txt +1 -0
- run.py +82 -0
- sample_guests.csv +11 -0
- templates/index.html +779 -0
DEPLOYMENT.md
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AWS Deployment Guide for Party Planner
|
| 2 |
+
|
| 3 |
+
This guide will walk you through deploying your Party Planner app on AWS using Elastic Beanstalk.
|
| 4 |
+
|
| 5 |
+
## Prerequisites
|
| 6 |
+
|
| 7 |
+
1. **AWS Account**: You need an active AWS account
|
| 8 |
+
2. **AWS CLI**: Install and configure AWS CLI
|
| 9 |
+
3. **EB CLI**: Install Elastic Beanstalk CLI
|
| 10 |
+
4. **Git**: For version control
|
| 11 |
+
|
| 12 |
+
## Step 1: Install AWS CLI and EB CLI
|
| 13 |
+
|
| 14 |
+
### Install AWS CLI
|
| 15 |
+
```bash
|
| 16 |
+
# macOS (using Homebrew)
|
| 17 |
+
brew install awscli
|
| 18 |
+
|
| 19 |
+
# Or download from AWS website
|
| 20 |
+
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
|
| 21 |
+
sudo installer -pkg AWSCLIV2.pkg -target /
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
### Install EB CLI
|
| 25 |
+
```bash
|
| 26 |
+
pip install awsebcli
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
### Configure AWS CLI
|
| 30 |
+
```bash
|
| 31 |
+
aws configure
|
| 32 |
+
# Enter your AWS Access Key ID
|
| 33 |
+
# Enter your AWS Secret Access Key
|
| 34 |
+
# Enter your default region (e.g., us-east-1)
|
| 35 |
+
# Enter your output format (json)
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
## Step 2: Prepare Your Application
|
| 39 |
+
|
| 40 |
+
Your application is already prepared with the necessary files:
|
| 41 |
+
- `application.py` - Main Flask application
|
| 42 |
+
- `requirements.txt` - Python dependencies
|
| 43 |
+
- `Procfile` - Tells EB how to run the app
|
| 44 |
+
- `.ebextensions/01_flask.config` - EB configuration
|
| 45 |
+
|
| 46 |
+
## Step 3: Initialize Elastic Beanstalk
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
# Navigate to your project directory
|
| 50 |
+
cd /path/to/your/LinkedInParty
|
| 51 |
+
|
| 52 |
+
# Initialize EB application
|
| 53 |
+
eb init
|
| 54 |
+
|
| 55 |
+
# Follow the prompts:
|
| 56 |
+
# 1. Select your region
|
| 57 |
+
# 2. Create new application (enter app name: party-planner)
|
| 58 |
+
# 3. Select Python platform
|
| 59 |
+
# 4. Select Python version (3.11 or 3.12)
|
| 60 |
+
# 5. Set up SSH (optional)
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
## Step 4: Create Environment
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
# Create and deploy your environment
|
| 67 |
+
eb create party-planner-env
|
| 68 |
+
|
| 69 |
+
# This will:
|
| 70 |
+
# 1. Create an EC2 instance
|
| 71 |
+
# 2. Set up load balancer
|
| 72 |
+
# 3. Configure security groups
|
| 73 |
+
# 4. Deploy your application
|
| 74 |
+
# 5. Provide you with a URL
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
## Step 5: Deploy Your Application
|
| 78 |
+
|
| 79 |
+
```bash
|
| 80 |
+
# Deploy your application
|
| 81 |
+
eb deploy
|
| 82 |
+
|
| 83 |
+
# Or if you want to deploy and open in browser
|
| 84 |
+
eb deploy --open
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
## Step 6: Monitor Your Application
|
| 88 |
+
|
| 89 |
+
```bash
|
| 90 |
+
# Check application status
|
| 91 |
+
eb status
|
| 92 |
+
|
| 93 |
+
# View application logs
|
| 94 |
+
eb logs
|
| 95 |
+
|
| 96 |
+
# Open application in browser
|
| 97 |
+
eb open
|
| 98 |
+
```
|
| 99 |
+
|
| 100 |
+
## Alternative Deployment Methods
|
| 101 |
+
|
| 102 |
+
### Method 2: AWS Lambda + API Gateway
|
| 103 |
+
|
| 104 |
+
If you prefer serverless deployment:
|
| 105 |
+
|
| 106 |
+
1. **Install AWS SAM CLI**
|
| 107 |
+
```bash
|
| 108 |
+
# macOS
|
| 109 |
+
brew install aws-sam-cli
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
2. **Create SAM template** (create `template.yaml`):
|
| 113 |
+
```yaml
|
| 114 |
+
AWSTemplateFormatVersion: '2010-09-09'
|
| 115 |
+
Transform: AWS::Serverless-2016-10-31
|
| 116 |
+
Resources:
|
| 117 |
+
PartyPlannerFunction:
|
| 118 |
+
Type: AWS::Serverless::Function
|
| 119 |
+
Properties:
|
| 120 |
+
CodeUri: ./
|
| 121 |
+
Handler: application.app
|
| 122 |
+
Runtime: python3.11
|
| 123 |
+
Events:
|
| 124 |
+
Api:
|
| 125 |
+
Type: Api
|
| 126 |
+
Properties:
|
| 127 |
+
Path: /{proxy+}
|
| 128 |
+
Method: ANY
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
3. **Deploy with SAM**
|
| 132 |
+
```bash
|
| 133 |
+
sam build
|
| 134 |
+
sam deploy --guided
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
### Method 3: AWS ECS (Docker)
|
| 138 |
+
|
| 139 |
+
1. **Create Dockerfile**:
|
| 140 |
+
```dockerfile
|
| 141 |
+
FROM python:3.11-slim
|
| 142 |
+
|
| 143 |
+
WORKDIR /app
|
| 144 |
+
COPY requirements.txt .
|
| 145 |
+
RUN pip install -r requirements.txt
|
| 146 |
+
|
| 147 |
+
COPY . .
|
| 148 |
+
|
| 149 |
+
EXPOSE 5000
|
| 150 |
+
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "application:app"]
|
| 151 |
+
```
|
| 152 |
+
|
| 153 |
+
2. **Build and push to ECR**
|
| 154 |
+
```bash
|
| 155 |
+
aws ecr create-repository --repository-name party-planner
|
| 156 |
+
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin YOUR_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com
|
| 157 |
+
docker build -t party-planner .
|
| 158 |
+
docker tag party-planner:latest YOUR_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/party-planner:latest
|
| 159 |
+
docker push YOUR_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/party-planner:latest
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
## Environment Variables (Optional)
|
| 163 |
+
|
| 164 |
+
You can set environment variables in the EB console or via CLI:
|
| 165 |
+
|
| 166 |
+
```bash
|
| 167 |
+
eb setenv SECRET_KEY=your-secret-key-here
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
## Custom Domain (Optional)
|
| 171 |
+
|
| 172 |
+
1. **Register domain in Route 53** or use existing domain
|
| 173 |
+
2. **Create SSL certificate** in AWS Certificate Manager
|
| 174 |
+
3. **Configure domain** in EB console or via CLI
|
| 175 |
+
|
| 176 |
+
```bash
|
| 177 |
+
eb config
|
| 178 |
+
# Add domain configuration
|
| 179 |
+
```
|
| 180 |
+
|
| 181 |
+
## Monitoring and Scaling
|
| 182 |
+
|
| 183 |
+
### Auto Scaling
|
| 184 |
+
```bash
|
| 185 |
+
# Configure auto scaling
|
| 186 |
+
eb config
|
| 187 |
+
# Set min/max instances based on your needs
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
### CloudWatch Monitoring
|
| 191 |
+
- Set up CloudWatch alarms
|
| 192 |
+
- Monitor application metrics
|
| 193 |
+
- Set up log aggregation
|
| 194 |
+
|
| 195 |
+
## Cost Optimization
|
| 196 |
+
|
| 197 |
+
### Free Tier
|
| 198 |
+
- AWS offers 12 months free tier
|
| 199 |
+
- 750 hours/month of t2.micro instances
|
| 200 |
+
- 20GB of storage
|
| 201 |
+
|
| 202 |
+
### Cost Saving Tips
|
| 203 |
+
1. Use t3.micro instances for development
|
| 204 |
+
2. Set up auto scaling to scale down during low usage
|
| 205 |
+
3. Use Spot Instances for non-critical workloads
|
| 206 |
+
4. Monitor costs in AWS Cost Explorer
|
| 207 |
+
|
| 208 |
+
## Troubleshooting
|
| 209 |
+
|
| 210 |
+
### Common Issues
|
| 211 |
+
|
| 212 |
+
1. **Deployment Fails**
|
| 213 |
+
```bash
|
| 214 |
+
eb logs
|
| 215 |
+
# Check for specific error messages
|
| 216 |
+
```
|
| 217 |
+
|
| 218 |
+
2. **Application Not Responding**
|
| 219 |
+
```bash
|
| 220 |
+
eb health
|
| 221 |
+
# Check application health
|
| 222 |
+
```
|
| 223 |
+
|
| 224 |
+
3. **Environment Issues**
|
| 225 |
+
```bash
|
| 226 |
+
eb events
|
| 227 |
+
# View recent events
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
### Useful Commands
|
| 231 |
+
|
| 232 |
+
```bash
|
| 233 |
+
# SSH into your instance
|
| 234 |
+
eb ssh
|
| 235 |
+
|
| 236 |
+
# View environment info
|
| 237 |
+
eb info
|
| 238 |
+
|
| 239 |
+
# List all environments
|
| 240 |
+
eb list
|
| 241 |
+
|
| 242 |
+
# Terminate environment
|
| 243 |
+
eb terminate party-planner-env
|
| 244 |
+
|
| 245 |
+
# Clone environment
|
| 246 |
+
eb clone party-planner-env --clone_name party-planner-env-2
|
| 247 |
+
```
|
| 248 |
+
|
| 249 |
+
## Security Best Practices
|
| 250 |
+
|
| 251 |
+
1. **Use IAM roles** instead of access keys
|
| 252 |
+
2. **Enable VPC** for network isolation
|
| 253 |
+
3. **Set up security groups** to restrict access
|
| 254 |
+
4. **Use HTTPS** for all communications
|
| 255 |
+
5. **Regular security updates**
|
| 256 |
+
|
| 257 |
+
## Backup and Recovery
|
| 258 |
+
|
| 259 |
+
1. **Database backups** (if using RDS)
|
| 260 |
+
2. **Application backups** (version control)
|
| 261 |
+
3. **Configuration backups** (EB configuration)
|
| 262 |
+
4. **Disaster recovery plan**
|
| 263 |
+
|
| 264 |
+
## Performance Optimization
|
| 265 |
+
|
| 266 |
+
1. **Enable CloudFront** for static content
|
| 267 |
+
2. **Use RDS** for database (if needed)
|
| 268 |
+
3. **Implement caching** (Redis/ElastiCache)
|
| 269 |
+
4. **Optimize application code**
|
| 270 |
+
|
| 271 |
+
## Maintenance
|
| 272 |
+
|
| 273 |
+
### Regular Tasks
|
| 274 |
+
1. **Update dependencies** monthly
|
| 275 |
+
2. **Monitor costs** weekly
|
| 276 |
+
3. **Review logs** for issues
|
| 277 |
+
4. **Update SSL certificates** before expiry
|
| 278 |
+
|
| 279 |
+
### Updates
|
| 280 |
+
```bash
|
| 281 |
+
# Update application
|
| 282 |
+
git add .
|
| 283 |
+
git commit -m "Update application"
|
| 284 |
+
eb deploy
|
| 285 |
+
|
| 286 |
+
# Update environment
|
| 287 |
+
eb upgrade
|
| 288 |
+
```
|
| 289 |
+
|
| 290 |
+
## Support
|
| 291 |
+
|
| 292 |
+
- **AWS Documentation**: https://docs.aws.amazon.com/elasticbeanstalk/
|
| 293 |
+
- **AWS Support**: Available with paid plans
|
| 294 |
+
- **Community Forums**: AWS Developer Forums
|
| 295 |
+
- **Stack Overflow**: Tag with aws-elasticbeanstalk
|
| 296 |
+
|
| 297 |
+
## Estimated Costs
|
| 298 |
+
|
| 299 |
+
### Small Application (t3.micro)
|
| 300 |
+
- **EC2**: ~$8-15/month
|
| 301 |
+
- **Load Balancer**: ~$18/month
|
| 302 |
+
- **Storage**: ~$1-5/month
|
| 303 |
+
- **Data Transfer**: ~$1-10/month
|
| 304 |
+
- **Total**: ~$30-50/month
|
| 305 |
+
|
| 306 |
+
### Free Tier
|
| 307 |
+
- **First 12 months**: Free (with limitations)
|
| 308 |
+
- **After free tier**: ~$30-50/month
|
| 309 |
+
|
| 310 |
+
## Next Steps
|
| 311 |
+
|
| 312 |
+
1. **Set up monitoring** with CloudWatch
|
| 313 |
+
2. **Configure custom domain**
|
| 314 |
+
3. **Set up CI/CD pipeline**
|
| 315 |
+
4. **Implement backup strategy**
|
| 316 |
+
5. **Plan for scaling**
|
| 317 |
+
|
| 318 |
+
Your Party Planner app is now ready for production deployment on AWS! 🚀
|
Procfile
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
web: gunicorn application:app
|
QUICKSTART.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Quick Start Guide
|
| 2 |
+
|
| 3 |
+
## 🚀 Get Started in 3 Steps
|
| 4 |
+
|
| 5 |
+
### 1. Install Dependencies
|
| 6 |
+
```bash
|
| 7 |
+
pip3 install -r requirements.txt
|
| 8 |
+
```
|
| 9 |
+
|
| 10 |
+
### 2. Run the App
|
| 11 |
+
```bash
|
| 12 |
+
python3 run.py
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
### 3. Open Your Browser
|
| 16 |
+
Navigate to: http://localhost:5000
|
| 17 |
+
|
| 18 |
+
## 🎯 What You'll See
|
| 19 |
+
|
| 20 |
+
1. **Login Page**: Enter your LinkedIn credentials
|
| 21 |
+
2. **Connections List**: View up to 100 of your LinkedIn connections
|
| 22 |
+
3. **Selection Interface**: Click to select connections (max 30)
|
| 23 |
+
4. **Table Arrangement**: Automatically organize into 3 tables of 10
|
| 24 |
+
|
| 25 |
+
## ⚡ Features at a Glance
|
| 26 |
+
|
| 27 |
+
- ✅ **Smart Categorization**: Groups connections by industry/role
|
| 28 |
+
- ✅ **Balanced Tables**: Ensures diverse table compositions
|
| 29 |
+
- ✅ **Interactive UI**: Modern, responsive design
|
| 30 |
+
- ✅ **Real-time Stats**: Track your selections
|
| 31 |
+
|
| 32 |
+
## 🔧 Troubleshooting
|
| 33 |
+
|
| 34 |
+
**Chrome not found?**
|
| 35 |
+
- Install Google Chrome browser
|
| 36 |
+
- Make sure it's in your PATH
|
| 37 |
+
|
| 38 |
+
**Login issues?**
|
| 39 |
+
- Verify your LinkedIn credentials
|
| 40 |
+
- Check if LinkedIn requires 2FA
|
| 41 |
+
|
| 42 |
+
**Connection loading slow?**
|
| 43 |
+
- This is normal - LinkedIn loads connections gradually
|
| 44 |
+
- Keep the browser window visible
|
| 45 |
+
|
| 46 |
+
## 📱 Mobile Friendly
|
| 47 |
+
|
| 48 |
+
The app works great on mobile devices too!
|
| 49 |
+
|
| 50 |
+
---
|
| 51 |
+
|
| 52 |
+
**Need help?** Check the full README.md for detailed instructions.
|
README.md
CHANGED
|
@@ -1,14 +1,182 @@
|
|
| 1 |
-
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
-
|
| 13 |
-
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Party Planner - Guest Table Arranger
|
| 2 |
+
|
| 3 |
+
A web application that helps you organize your guest list into tables for networking events. The app accepts CSV file uploads with guest information and creates visual circular table arrangements showing where each person will be seated.
|
| 4 |
+
|
| 5 |
+
## Features
|
| 6 |
+
|
| 7 |
+
- 📁 Easy CSV file upload with drag & drop support
|
| 8 |
+
- 📋 Parse guest information from name and message columns
|
| 9 |
+
- ✅ Interactive guest selection with checkboxes
|
| 10 |
+
- 🎯 Smart table arrangement algorithm
|
| 11 |
+
- 🎨 Visual circular table layouts with seating positions
|
| 12 |
+
- 📱 Responsive design for mobile and desktop
|
| 13 |
+
- 🎨 Modern, intuitive UI
|
| 14 |
+
|
| 15 |
+
## Prerequisites
|
| 16 |
+
|
| 17 |
+
- Python 3.7 or higher
|
| 18 |
+
- Web browser
|
| 19 |
+
|
| 20 |
+
## Installation
|
| 21 |
+
|
| 22 |
+
1. **Clone or download this repository**
|
| 23 |
+
```bash
|
| 24 |
+
git clone <repository-url>
|
| 25 |
+
cd LinkedInParty
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
2. **Install Python dependencies**
|
| 29 |
+
```bash
|
| 30 |
+
pip install -r requirements.txt
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
## Usage
|
| 34 |
+
|
| 35 |
+
1. **Start the application**
|
| 36 |
+
```bash
|
| 37 |
+
python3 app.py
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
2. **Open your browser**
|
| 41 |
+
Navigate to `http://localhost:5000`
|
| 42 |
+
|
| 43 |
+
3. **Prepare your CSV file**
|
| 44 |
+
Your CSV should have exactly 2 columns:
|
| 45 |
+
- **First column**: guest names
|
| 46 |
+
- **Second column**: descriptions, roles, or messages
|
| 47 |
+
|
| 48 |
+
4. **Upload your guest list**
|
| 49 |
+
- Click "Upload Guest List" or drag and drop your CSV file
|
| 50 |
+
- The app will parse and display up to 100 guests
|
| 51 |
+
|
| 52 |
+
5. **Select guests for your event**
|
| 53 |
+
- Click on guest cards to select/deselect them
|
| 54 |
+
- You can select up to 30 guests (3 tables × 10 people)
|
| 55 |
+
- The stats panel shows your selection progress
|
| 56 |
+
|
| 57 |
+
6. **Arrange tables**
|
| 58 |
+
- Click "Arrange Tables" to automatically organize your selected guests
|
| 59 |
+
- The app will create three visual circular tables
|
| 60 |
+
- Each table shows seating positions with guest initials
|
| 61 |
+
- Hover over seats to see full names and descriptions
|
| 62 |
+
|
| 63 |
+
## CSV Format Example
|
| 64 |
+
|
| 65 |
+
```csv
|
| 66 |
+
name,message
|
| 67 |
+
John Smith,Software Engineer at TechCorp
|
| 68 |
+
Sarah Johnson,Marketing Director at Creative Agency
|
| 69 |
+
Michael Brown,CEO of StartupXYZ
|
| 70 |
+
Emily Davis,Data Scientist at AI Labs
|
| 71 |
+
David Wilson,Product Manager at Innovation Inc
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
## Visual Table Layout
|
| 75 |
+
|
| 76 |
+
The app creates beautiful circular table visualizations:
|
| 77 |
+
- 🟢 **Green circles** represent individual seats
|
| 78 |
+
- 🔵 **Blue center** shows the table number
|
| 79 |
+
- 👤 **Initials** displayed on each seat
|
| 80 |
+
- 💬 **Hover tooltips** show full names and descriptions
|
| 81 |
+
- 🎯 **Smart positioning** arranges seats evenly around the table
|
| 82 |
+
|
| 83 |
+
## How it Works
|
| 84 |
+
|
| 85 |
+
### CSV Processing
|
| 86 |
+
- Supports multiple CSV delimiters (comma, semicolon, tab)
|
| 87 |
+
- Automatically detects column headers
|
| 88 |
+
- Reads first column for names and second column for descriptions
|
| 89 |
+
- Handles various encoding formats
|
| 90 |
+
|
| 91 |
+
### Table Arrangement Algorithm
|
| 92 |
+
The app uses a smart distribution algorithm:
|
| 93 |
+
1. Categorizes guests by industry/role type based on message content
|
| 94 |
+
2. Distributes major categories evenly across 3 tables
|
| 95 |
+
3. Ensures each table has a maximum of 10 people
|
| 96 |
+
4. Balances the number of people per table
|
| 97 |
+
|
| 98 |
+
### Smart Categorization
|
| 99 |
+
Based on message content, guests are categorized as:
|
| 100 |
+
- **Tech**: engineer, developer, programmer, software, tech, it, data, ai, ml, technology, scientist
|
| 101 |
+
- **Business**: manager, director, ceo, founder, executive, business, strategy, operations, consultant, product
|
| 102 |
+
- **Creative**: designer, creative, marketing, content, writer, artist, media, communications, strategist
|
| 103 |
+
- **Sales**: sales, account, client, business development, partnership, account manager
|
| 104 |
+
- **Finance**: finance, accounting, investment, banking, financial, analyst, cfo
|
| 105 |
+
- **Other**: any roles not matching the above categories
|
| 106 |
+
|
| 107 |
+
### Visual Seating Layout
|
| 108 |
+
- **Circular tables** with realistic seating arrangements
|
| 109 |
+
- **Even distribution** of seats around each table
|
| 110 |
+
- **Interactive elements** with hover effects
|
| 111 |
+
- **Responsive design** that works on all devices
|
| 112 |
+
|
| 113 |
+
## Troubleshooting
|
| 114 |
+
|
| 115 |
+
### Common Issues
|
| 116 |
+
|
| 117 |
+
1. **CSV Format Problems**
|
| 118 |
+
- Ensure your CSV has exactly 2 columns (name and message)
|
| 119 |
+
- Check that the first column contains guest names
|
| 120 |
+
- Verify the second column contains role descriptions
|
| 121 |
+
|
| 122 |
+
2. **File Upload Issues**
|
| 123 |
+
- Make sure you're uploading a .csv file
|
| 124 |
+
- Check that the file isn't corrupted or empty
|
| 125 |
+
- Try a smaller file first to test the format
|
| 126 |
+
|
| 127 |
+
3. **Parsing Errors**
|
| 128 |
+
- The app will try different delimiters automatically
|
| 129 |
+
- If parsing fails, check your CSV format in a text editor
|
| 130 |
+
- Ensure there are no special characters causing issues
|
| 131 |
+
|
| 132 |
+
### Performance Tips
|
| 133 |
+
|
| 134 |
+
- The app can handle up to 100 guests efficiently
|
| 135 |
+
- Larger files may take a moment to process
|
| 136 |
+
- Keep your CSV file under 1MB for best performance
|
| 137 |
+
|
| 138 |
+
## Technical Details
|
| 139 |
+
|
| 140 |
+
### Backend (Flask)
|
| 141 |
+
- `app.py`: Main Flask application with CSV processing
|
| 142 |
+
- Uses Python's built-in CSV module for parsing
|
| 143 |
+
- RESTful API endpoints for frontend communication
|
| 144 |
+
|
| 145 |
+
### Frontend (HTML/CSS/JavaScript)
|
| 146 |
+
- Modern responsive design
|
| 147 |
+
- Drag & drop file upload
|
| 148 |
+
- Interactive guest selection
|
| 149 |
+
- Circular table visualization with CSS positioning
|
| 150 |
+
- Real-time statistics updates
|
| 151 |
+
|
| 152 |
+
### Dependencies
|
| 153 |
+
- `flask`: Web framework
|
| 154 |
+
- `flask-cors`: Cross-origin resource sharing
|
| 155 |
+
- `werkzeug`: File upload handling
|
| 156 |
+
|
| 157 |
+
## Limitations
|
| 158 |
+
|
| 159 |
+
- Maximum 30 guests can be arranged (3 tables × 10 people)
|
| 160 |
+
- Maximum 100 guests can be loaded from CSV
|
| 161 |
+
- CSV files only (no Excel or other formats)
|
| 162 |
+
- Guest data is not persistent between sessions
|
| 163 |
+
|
| 164 |
+
## Future Enhancements
|
| 165 |
+
|
| 166 |
+
- [ ] Support for Excel files (.xlsx, .xls)
|
| 167 |
+
- [ ] Save/load table arrangements
|
| 168 |
+
- [ ] Export to CSV/PDF
|
| 169 |
+
- [ ] Custom table sizes
|
| 170 |
+
- [ ] Guest filtering by industry/company
|
| 171 |
+
- [ ] Integration with calendar systems
|
| 172 |
+
- [ ] Email invitations to guests
|
| 173 |
+
- [ ] 3D table visualization
|
| 174 |
+
- [ ] Drag & drop seating arrangements
|
| 175 |
+
|
| 176 |
+
## Contributing
|
| 177 |
+
|
| 178 |
+
Feel free to submit issues and enhancement requests!
|
| 179 |
+
|
| 180 |
+
## License
|
| 181 |
+
|
| 182 |
+
This project is for educational and personal use purposes.
|
README_HF.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🎉 Party Planner - Guest Table Arranger
|
| 2 |
+
|
| 3 |
+
A smart web application that helps you organize your guest list into optimal table arrangements for networking events and parties. Built with Gradio and deployed on Hugging Face Spaces.
|
| 4 |
+
|
| 5 |
+
## ✨ Features
|
| 6 |
+
|
| 7 |
+
- 📁 **Easy CSV Upload**: Simply paste your CSV content or upload a file
|
| 8 |
+
- 🧠 **Smart Table Arrangement**: AI-powered algorithm that categorizes guests by industry/role
|
| 9 |
+
- 🎯 **Optimal Distribution**: Automatically creates tables of 10 with balanced industry mix
|
| 10 |
+
- 📊 **Visual Results**: Clear, formatted output showing table assignments
|
| 11 |
+
- 🎨 **Modern Interface**: Beautiful, responsive Gradio interface
|
| 12 |
+
- 📱 **Mobile Friendly**: Works perfectly on all devices
|
| 13 |
+
|
| 14 |
+
## 🚀 How to Use
|
| 15 |
+
|
| 16 |
+
1. **Prepare Your CSV**: Create a CSV file with exactly 2 columns:
|
| 17 |
+
- **First column**: Guest names
|
| 18 |
+
- **Second column**: Job titles, roles, or descriptions
|
| 19 |
+
|
| 20 |
+
2. **Upload Your Data**:
|
| 21 |
+
- Paste your CSV content directly into the text box, OR
|
| 22 |
+
- Upload your CSV file using the file upload button
|
| 23 |
+
|
| 24 |
+
3. **Arrange Tables**: Click the "🎯 Arrange Tables" button to see the smart arrangement
|
| 25 |
+
|
| 26 |
+
4. **View Results**: See your guests organized into optimal table seating with industry-balanced distribution
|
| 27 |
+
|
| 28 |
+
## 📄 CSV Format Example
|
| 29 |
+
|
| 30 |
+
```csv
|
| 31 |
+
name,message
|
| 32 |
+
John Smith,Software Engineer at TechCorp
|
| 33 |
+
Sarah Johnson,Marketing Director at Creative Agency
|
| 34 |
+
Michael Brown,CEO of StartupXYZ
|
| 35 |
+
Emily Davis,Data Scientist at AI Labs
|
| 36 |
+
David Wilson,Product Manager at Innovation Inc
|
| 37 |
+
```
|
| 38 |
+
|
| 39 |
+
## 🧠 Smart Categorization
|
| 40 |
+
|
| 41 |
+
The app automatically categorizes guests based on their job descriptions:
|
| 42 |
+
|
| 43 |
+
- **Tech**: engineer, developer, programmer, software, tech, data, AI, ML, scientist
|
| 44 |
+
- **Business**: manager, director, CEO, founder, executive, strategy, operations, consultant
|
| 45 |
+
- **Creative**: designer, creative, marketing, content, writer, artist, media
|
| 46 |
+
- **Sales**: sales, account, client, business development, partnership
|
| 47 |
+
- **Finance**: finance, accounting, investment, banking, analyst, CFO
|
| 48 |
+
- **Other**: any roles not matching the above categories
|
| 49 |
+
|
| 50 |
+
## 🎯 Table Arrangement Algorithm
|
| 51 |
+
|
| 52 |
+
1. **Categorizes** guests by industry/role type
|
| 53 |
+
2. **Distributes** major categories evenly across tables
|
| 54 |
+
3. **Balances** table sizes (maximum 10 guests per table)
|
| 55 |
+
4. **Creates** unlimited tables as needed
|
| 56 |
+
5. **Ensures** diverse networking opportunities
|
| 57 |
+
|
| 58 |
+
## 📊 Sample Output
|
| 59 |
+
|
| 60 |
+
```
|
| 61 |
+
🎉 Successfully processed 25 guests!
|
| 62 |
+
|
| 63 |
+
📊 Created 3 table(s) with smart distribution:
|
| 64 |
+
|
| 65 |
+
🍽️ **Table 1** (9 guests):
|
| 66 |
+
1. **John Smith** - Software Engineer at TechCorp
|
| 67 |
+
2. **Sarah Johnson** - Marketing Director at Creative Agency
|
| 68 |
+
3. **Michael Brown** - CEO of StartupXYZ
|
| 69 |
+
...
|
| 70 |
+
|
| 71 |
+
🍽️ **Table 2** (8 guests):
|
| 72 |
+
1. **Emily Davis** - Data Scientist at AI Labs
|
| 73 |
+
2. **David Wilson** - Product Manager at Innovation Inc
|
| 74 |
+
...
|
| 75 |
+
|
| 76 |
+
🍽️ **Table 3** (8 guests):
|
| 77 |
+
1. **Lisa Chen** - UX Designer at Design Studio
|
| 78 |
+
...
|
| 79 |
+
|
| 80 |
+
📈 **Table Statistics:**
|
| 81 |
+
Table 1: 9 guests
|
| 82 |
+
Table 2: 8 guests
|
| 83 |
+
Table 3: 8 guests
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
## 🛠️ Technical Details
|
| 87 |
+
|
| 88 |
+
- **Framework**: Gradio (Python)
|
| 89 |
+
- **Algorithm**: Smart categorization and distribution
|
| 90 |
+
- **Deployment**: Hugging Face Spaces
|
| 91 |
+
- **Dependencies**: gradio>=4.0.0
|
| 92 |
+
|
| 93 |
+
## 🎨 Features
|
| 94 |
+
|
| 95 |
+
- **Drag & Drop**: Easy file upload
|
| 96 |
+
- **Real-time Processing**: Instant table arrangement
|
| 97 |
+
- **Error Handling**: Clear error messages for invalid formats
|
| 98 |
+
- **Sample Data**: Built-in sample CSV for testing
|
| 99 |
+
- **Responsive Design**: Works on desktop and mobile
|
| 100 |
+
|
| 101 |
+
## 🔧 Local Development
|
| 102 |
+
|
| 103 |
+
To run this locally:
|
| 104 |
+
|
| 105 |
+
```bash
|
| 106 |
+
pip install gradio
|
| 107 |
+
python app.py
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
## 📝 License
|
| 111 |
+
|
| 112 |
+
This project is for educational and personal use purposes.
|
| 113 |
+
|
| 114 |
+
---
|
| 115 |
+
|
| 116 |
+
**Made with ❤️ for better networking events!**
|
app.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hugging Face Spaces Entry Point
|
| 2 |
+
# This file launches the Gradio interface for the Party Planner app
|
| 3 |
+
|
| 4 |
+
from app_gradio import demo
|
| 5 |
+
|
| 6 |
+
# Launch the Gradio app
|
| 7 |
+
if __name__ == "__main__":
|
| 8 |
+
demo.launch()
|
app_gradio.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import csv
|
| 3 |
+
import io
|
| 4 |
+
import json
|
| 5 |
+
from typing import List, Dict, Any
|
| 6 |
+
|
| 7 |
+
def parse_csv_file(file_content: str) -> Dict[str, Any]:
|
| 8 |
+
"""Parse CSV content and extract guest information from name and message columns"""
|
| 9 |
+
try:
|
| 10 |
+
# Create a StringIO object to read the CSV
|
| 11 |
+
csv_file = io.StringIO(file_content)
|
| 12 |
+
|
| 13 |
+
# Read CSV with different possible delimiters
|
| 14 |
+
for delimiter in [',', ';', '\t']:
|
| 15 |
+
try:
|
| 16 |
+
csv_file.seek(0) # Reset file pointer
|
| 17 |
+
reader = csv.DictReader(csv_file, delimiter=delimiter)
|
| 18 |
+
|
| 19 |
+
# Get the header row
|
| 20 |
+
headers = reader.fieldnames
|
| 21 |
+
if not headers or len(headers) < 2:
|
| 22 |
+
continue
|
| 23 |
+
|
| 24 |
+
# Use the first two columns: name and message
|
| 25 |
+
name_column = headers[0]
|
| 26 |
+
message_column = headers[1]
|
| 27 |
+
|
| 28 |
+
# Parse the data and filter out entries with blank names or descriptions
|
| 29 |
+
guests = []
|
| 30 |
+
for i, row in enumerate(reader):
|
| 31 |
+
name = row.get(name_column, '').strip()
|
| 32 |
+
title = row.get(message_column, '').strip()
|
| 33 |
+
|
| 34 |
+
# Only add if both name and description are not blank
|
| 35 |
+
if name and title:
|
| 36 |
+
guests.append({
|
| 37 |
+
'id': i,
|
| 38 |
+
'name': name,
|
| 39 |
+
'title': title
|
| 40 |
+
})
|
| 41 |
+
|
| 42 |
+
if guests:
|
| 43 |
+
return {'success': True, 'guests': guests, 'total': len(guests)}
|
| 44 |
+
|
| 45 |
+
except Exception as e:
|
| 46 |
+
continue
|
| 47 |
+
|
| 48 |
+
return {'success': False, 'error': 'Could not parse CSV file. Please ensure it has at least 2 columns (name and message).'}
|
| 49 |
+
|
| 50 |
+
except Exception as e:
|
| 51 |
+
return {'success': False, 'error': f'Error parsing CSV: {str(e)}'}
|
| 52 |
+
|
| 53 |
+
def arrange_guests_into_tables(guests: List[Dict]) -> List[List[Dict]]:
|
| 54 |
+
"""Arrange guests into tables of 10 people each with smart distribution"""
|
| 55 |
+
if not guests:
|
| 56 |
+
return []
|
| 57 |
+
|
| 58 |
+
# Categorize guests by industry/role type based on message content
|
| 59 |
+
categories = {
|
| 60 |
+
'tech': ['engineer', 'developer', 'programmer', 'software', 'tech', 'it', 'data', 'ai', 'ml', 'technology', 'scientist'],
|
| 61 |
+
'business': ['manager', 'director', 'ceo', 'founder', 'executive', 'business', 'strategy', 'operations', 'consultant', 'product'],
|
| 62 |
+
'creative': ['designer', 'creative', 'marketing', 'content', 'writer', 'artist', 'media', 'communications', 'strategist'],
|
| 63 |
+
'sales': ['sales', 'account', 'client', 'business development', 'partnership', 'account manager'],
|
| 64 |
+
'finance': ['finance', 'accounting', 'investment', 'banking', 'financial', 'analyst', 'cfo'],
|
| 65 |
+
'other': []
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
categorized_guests = {cat: [] for cat in categories.keys()}
|
| 69 |
+
|
| 70 |
+
# Categorize each guest based on their message/description
|
| 71 |
+
for guest in guests:
|
| 72 |
+
title_lower = guest['title'].lower()
|
| 73 |
+
categorized = False
|
| 74 |
+
|
| 75 |
+
for category, keywords in categories.items():
|
| 76 |
+
if category == 'other':
|
| 77 |
+
continue
|
| 78 |
+
if any(keyword in title_lower for keyword in keywords):
|
| 79 |
+
categorized_guests[category].append(guest)
|
| 80 |
+
categorized = True
|
| 81 |
+
break
|
| 82 |
+
|
| 83 |
+
if not categorized:
|
| 84 |
+
categorized_guests['other'].append(guest)
|
| 85 |
+
|
| 86 |
+
# Calculate how many tables we need
|
| 87 |
+
total_guests = len(guests)
|
| 88 |
+
num_tables = (total_guests + 9) // 10 # Ceiling division to get number of tables needed
|
| 89 |
+
|
| 90 |
+
# Initialize tables
|
| 91 |
+
tables = [[] for _ in range(num_tables)]
|
| 92 |
+
|
| 93 |
+
# Distribute guests strategically across all tables
|
| 94 |
+
# First, distribute major categories evenly
|
| 95 |
+
major_categories = ['tech', 'business', 'creative', 'sales']
|
| 96 |
+
|
| 97 |
+
for category in major_categories:
|
| 98 |
+
guests_in_category = categorized_guests[category]
|
| 99 |
+
if guests_in_category:
|
| 100 |
+
# Distribute evenly across all tables
|
| 101 |
+
for i, guest in enumerate(guests_in_category):
|
| 102 |
+
table_index = i % num_tables
|
| 103 |
+
if len(tables[table_index]) < 10:
|
| 104 |
+
tables[table_index].append(guest)
|
| 105 |
+
|
| 106 |
+
# Then distribute remaining guests (finance, other)
|
| 107 |
+
remaining_guests = []
|
| 108 |
+
for category in ['finance', 'other']:
|
| 109 |
+
remaining_guests.extend(categorized_guests[category])
|
| 110 |
+
|
| 111 |
+
# Also add any guests that didn't fit in the first round
|
| 112 |
+
for table in tables:
|
| 113 |
+
for guest in guests:
|
| 114 |
+
if guest not in [g for table_guests in tables for g in table_guests]:
|
| 115 |
+
if len(table) < 10:
|
| 116 |
+
table.append(guest)
|
| 117 |
+
break
|
| 118 |
+
|
| 119 |
+
# Fill remaining slots with any leftover guests
|
| 120 |
+
for guest in guests:
|
| 121 |
+
if guest not in [g for table_guests in tables for g in table_guests]:
|
| 122 |
+
for table in tables:
|
| 123 |
+
if len(table) < 10:
|
| 124 |
+
table.append(guest)
|
| 125 |
+
break
|
| 126 |
+
|
| 127 |
+
return tables
|
| 128 |
+
|
| 129 |
+
def process_csv_and_arrange_tables(csv_content: str) -> str:
|
| 130 |
+
"""Main function to process CSV and arrange tables"""
|
| 131 |
+
try:
|
| 132 |
+
# Parse CSV
|
| 133 |
+
result = parse_csv_file(csv_content)
|
| 134 |
+
|
| 135 |
+
if not result['success']:
|
| 136 |
+
return f"❌ Error: {result['error']}"
|
| 137 |
+
|
| 138 |
+
guests = result['guests']
|
| 139 |
+
total_guests = result['total']
|
| 140 |
+
|
| 141 |
+
if total_guests == 0:
|
| 142 |
+
return "❌ No valid guests found in CSV. Please ensure you have at least 2 columns (name and message) with non-empty values."
|
| 143 |
+
|
| 144 |
+
# Arrange tables
|
| 145 |
+
tables = arrange_guests_into_tables(guests)
|
| 146 |
+
|
| 147 |
+
if not tables:
|
| 148 |
+
return "❌ No tables could be created."
|
| 149 |
+
|
| 150 |
+
# Generate output
|
| 151 |
+
output = f"🎉 Successfully processed {total_guests} guests!\n\n"
|
| 152 |
+
output += f"📊 Created {len(tables)} table(s) with smart distribution:\n\n"
|
| 153 |
+
|
| 154 |
+
for i, table in enumerate(tables, 1):
|
| 155 |
+
output += f"🍽️ **Table {i}** ({len(table)} guests):\n"
|
| 156 |
+
for j, guest in enumerate(table, 1):
|
| 157 |
+
output += f" {j}. **{guest['name']}** - {guest['title']}\n"
|
| 158 |
+
output += "\n"
|
| 159 |
+
|
| 160 |
+
# Add statistics
|
| 161 |
+
output += "📈 **Table Statistics:**\n"
|
| 162 |
+
for i, table in enumerate(tables, 1):
|
| 163 |
+
output += f" Table {i}: {len(table)} guests\n"
|
| 164 |
+
|
| 165 |
+
return output
|
| 166 |
+
|
| 167 |
+
except Exception as e:
|
| 168 |
+
return f"❌ Error processing request: {str(e)}"
|
| 169 |
+
|
| 170 |
+
def create_sample_csv() -> str:
|
| 171 |
+
"""Create a sample CSV for users to download"""
|
| 172 |
+
sample_data = """name,message
|
| 173 |
+
John Smith,Software Engineer at TechCorp
|
| 174 |
+
Sarah Johnson,Marketing Director at Creative Agency
|
| 175 |
+
Michael Brown,CEO of StartupXYZ
|
| 176 |
+
Emily Davis,Data Scientist at AI Labs
|
| 177 |
+
David Wilson,Product Manager at Innovation Inc
|
| 178 |
+
Lisa Chen,UX Designer at Design Studio
|
| 179 |
+
Robert Taylor,Sales Manager at SalesForce
|
| 180 |
+
Amanda Rodriguez,Financial Analyst at Finance Corp
|
| 181 |
+
James Lee,Content Strategist at Media Group
|
| 182 |
+
Jennifer White,Business Development at Growth Co"""
|
| 183 |
+
return sample_data
|
| 184 |
+
|
| 185 |
+
# Create the Gradio interface
|
| 186 |
+
with gr.Blocks(
|
| 187 |
+
title="Party Planner - Guest Table Arranger",
|
| 188 |
+
theme=gr.themes.Soft(),
|
| 189 |
+
css="""
|
| 190 |
+
.gradio-container {
|
| 191 |
+
max-width: 1200px !important;
|
| 192 |
+
margin: 0 auto !important;
|
| 193 |
+
}
|
| 194 |
+
.main-header {
|
| 195 |
+
text-align: center;
|
| 196 |
+
margin-bottom: 2rem;
|
| 197 |
+
}
|
| 198 |
+
.sample-csv {
|
| 199 |
+
background: #f0f8ff;
|
| 200 |
+
padding: 1rem;
|
| 201 |
+
border-radius: 8px;
|
| 202 |
+
margin: 1rem 0;
|
| 203 |
+
}
|
| 204 |
+
"""
|
| 205 |
+
) as demo:
|
| 206 |
+
|
| 207 |
+
gr.HTML("""
|
| 208 |
+
<div class="main-header">
|
| 209 |
+
<h1>🎉 Party Planner - Guest Table Arranger</h1>
|
| 210 |
+
<p>Upload your guest list CSV and let AI arrange them into optimal table seating!</p>
|
| 211 |
+
</div>
|
| 212 |
+
""")
|
| 213 |
+
|
| 214 |
+
with gr.Row():
|
| 215 |
+
with gr.Column(scale=1):
|
| 216 |
+
gr.HTML("""
|
| 217 |
+
<h3>📋 How to use:</h3>
|
| 218 |
+
<ol>
|
| 219 |
+
<li>Prepare a CSV file with 2 columns: <strong>name</strong> and <strong>message/description</strong></li>
|
| 220 |
+
<li>Upload your CSV file below</li>
|
| 221 |
+
<li>Click "Arrange Tables" to see the smart seating arrangement</li>
|
| 222 |
+
</ol>
|
| 223 |
+
|
| 224 |
+
<div class="sample-csv">
|
| 225 |
+
<h4>📄 Sample CSV Format:</h4>
|
| 226 |
+
<pre>name,message
|
| 227 |
+
John Smith,Software Engineer at TechCorp
|
| 228 |
+
Sarah Johnson,Marketing Director at Creative Agency
|
| 229 |
+
Michael Brown,CEO of StartupXYZ</pre>
|
| 230 |
+
</div>
|
| 231 |
+
""")
|
| 232 |
+
|
| 233 |
+
sample_csv = gr.Textbox(
|
| 234 |
+
label="Sample CSV Content",
|
| 235 |
+
value=create_sample_csv(),
|
| 236 |
+
lines=10,
|
| 237 |
+
interactive=False
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
download_sample = gr.Button("📥 Download Sample CSV", variant="secondary")
|
| 241 |
+
|
| 242 |
+
with gr.Column(scale=2):
|
| 243 |
+
csv_input = gr.Textbox(
|
| 244 |
+
label="📁 Paste your CSV content here (or upload file below)",
|
| 245 |
+
placeholder="Paste your CSV content here...",
|
| 246 |
+
lines=10
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
file_input = gr.File(
|
| 250 |
+
label="📂 Or upload CSV file",
|
| 251 |
+
file_types=[".csv"],
|
| 252 |
+
file_count="single"
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
arrange_btn = gr.Button("🎯 Arrange Tables", variant="primary", size="lg")
|
| 256 |
+
|
| 257 |
+
output = gr.Markdown(
|
| 258 |
+
label="📊 Table Arrangement Results",
|
| 259 |
+
value="Upload your guest list to see the table arrangement!"
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
# Event handlers
|
| 263 |
+
def handle_file_upload(file):
|
| 264 |
+
if file is None:
|
| 265 |
+
return ""
|
| 266 |
+
try:
|
| 267 |
+
with open(file.name, 'r', encoding='utf-8') as f:
|
| 268 |
+
content = f.read()
|
| 269 |
+
return content
|
| 270 |
+
except Exception as e:
|
| 271 |
+
return f"Error reading file: {str(e)}"
|
| 272 |
+
|
| 273 |
+
def download_sample_csv():
|
| 274 |
+
return create_sample_csv()
|
| 275 |
+
|
| 276 |
+
# Connect events
|
| 277 |
+
file_input.change(
|
| 278 |
+
fn=handle_file_upload,
|
| 279 |
+
inputs=[file_input],
|
| 280 |
+
outputs=[csv_input]
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
arrange_btn.click(
|
| 284 |
+
fn=process_csv_and_arrange_tables,
|
| 285 |
+
inputs=[csv_input],
|
| 286 |
+
outputs=[output]
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
download_sample.click(
|
| 290 |
+
fn=download_sample_csv,
|
| 291 |
+
outputs=[csv_input]
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
# Launch the app
|
| 295 |
+
if __name__ == "__main__":
|
| 296 |
+
demo.launch()
|
application.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request, jsonify
|
| 2 |
+
from flask_cors import CORS
|
| 3 |
+
import csv
|
| 4 |
+
import io
|
| 5 |
+
import os
|
| 6 |
+
from werkzeug.utils import secure_filename
|
| 7 |
+
|
| 8 |
+
app = Flask(__name__)
|
| 9 |
+
CORS(app)
|
| 10 |
+
|
| 11 |
+
# Configure upload settings
|
| 12 |
+
UPLOAD_FOLDER = '/tmp/uploads' # Use /tmp for AWS Lambda compatibility
|
| 13 |
+
ALLOWED_EXTENSIONS = {'csv'}
|
| 14 |
+
|
| 15 |
+
if not os.path.exists(UPLOAD_FOLDER):
|
| 16 |
+
os.makedirs(UPLOAD_FOLDER)
|
| 17 |
+
|
| 18 |
+
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
| 19 |
+
|
| 20 |
+
def allowed_file(filename):
|
| 21 |
+
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
|
| 22 |
+
|
| 23 |
+
def parse_csv_file(file_content):
|
| 24 |
+
"""Parse CSV content and extract guest information from name and message columns"""
|
| 25 |
+
try:
|
| 26 |
+
# Decode the file content
|
| 27 |
+
if isinstance(file_content, bytes):
|
| 28 |
+
file_content = file_content.decode('utf-8')
|
| 29 |
+
|
| 30 |
+
# Create a StringIO object to read the CSV
|
| 31 |
+
csv_file = io.StringIO(file_content)
|
| 32 |
+
|
| 33 |
+
# Read CSV with different possible delimiters
|
| 34 |
+
for delimiter in [',', ';', '\t']:
|
| 35 |
+
try:
|
| 36 |
+
csv_file.seek(0) # Reset file pointer
|
| 37 |
+
reader = csv.DictReader(csv_file, delimiter=delimiter)
|
| 38 |
+
|
| 39 |
+
# Get the header row
|
| 40 |
+
headers = reader.fieldnames
|
| 41 |
+
if not headers or len(headers) < 2:
|
| 42 |
+
continue
|
| 43 |
+
|
| 44 |
+
# Use the first two columns: name and message
|
| 45 |
+
name_column = headers[0]
|
| 46 |
+
message_column = headers[1]
|
| 47 |
+
|
| 48 |
+
# Parse the data and filter out entries with blank names or descriptions
|
| 49 |
+
guests = []
|
| 50 |
+
for i, row in enumerate(reader):
|
| 51 |
+
name = row.get(name_column, '').strip()
|
| 52 |
+
title = row.get(message_column, '').strip()
|
| 53 |
+
|
| 54 |
+
# Only add if both name and description are not blank
|
| 55 |
+
if name and title:
|
| 56 |
+
guests.append({
|
| 57 |
+
'id': i,
|
| 58 |
+
'name': name,
|
| 59 |
+
'title': title
|
| 60 |
+
})
|
| 61 |
+
|
| 62 |
+
if guests:
|
| 63 |
+
return {'success': True, 'guests': guests, 'total': len(guests)}
|
| 64 |
+
|
| 65 |
+
except Exception as e:
|
| 66 |
+
continue
|
| 67 |
+
|
| 68 |
+
return {'success': False, 'error': 'Could not parse CSV file. Please ensure it has at least 2 columns (name and message).'}
|
| 69 |
+
|
| 70 |
+
except Exception as e:
|
| 71 |
+
return {'success': False, 'error': f'Error parsing CSV: {str(e)}'}
|
| 72 |
+
|
| 73 |
+
@app.route('/')
|
| 74 |
+
def index():
|
| 75 |
+
return render_template('index.html')
|
| 76 |
+
|
| 77 |
+
@app.route('/upload-csv', methods=['POST'])
|
| 78 |
+
def upload_csv():
|
| 79 |
+
"""Handle CSV file upload"""
|
| 80 |
+
try:
|
| 81 |
+
if 'file' not in request.files:
|
| 82 |
+
return jsonify({'success': False, 'error': 'No file uploaded'})
|
| 83 |
+
|
| 84 |
+
file = request.files['file']
|
| 85 |
+
|
| 86 |
+
if file.filename == '':
|
| 87 |
+
return jsonify({'success': False, 'error': 'No file selected'})
|
| 88 |
+
|
| 89 |
+
if not allowed_file(file.filename):
|
| 90 |
+
return jsonify({'success': False, 'error': 'Please upload a CSV file'})
|
| 91 |
+
|
| 92 |
+
# Read file content
|
| 93 |
+
file_content = file.read()
|
| 94 |
+
|
| 95 |
+
# Parse CSV
|
| 96 |
+
result = parse_csv_file(file_content)
|
| 97 |
+
|
| 98 |
+
if result['success']:
|
| 99 |
+
return jsonify({
|
| 100 |
+
'success': True,
|
| 101 |
+
'guests': result['guests'],
|
| 102 |
+
'total': result['total'],
|
| 103 |
+
'message': f'Successfully loaded {result["total"]} guests from CSV (filtered out entries with blank names or descriptions)'
|
| 104 |
+
})
|
| 105 |
+
else:
|
| 106 |
+
return jsonify(result)
|
| 107 |
+
|
| 108 |
+
except Exception as e:
|
| 109 |
+
return jsonify({'success': False, 'error': f'Upload error: {str(e)}'})
|
| 110 |
+
|
| 111 |
+
@app.route('/arrange-tables', methods=['POST'])
|
| 112 |
+
def arrange_tables():
|
| 113 |
+
data = request.get_json()
|
| 114 |
+
selected_guests = data.get('selected_guests', [])
|
| 115 |
+
|
| 116 |
+
if not selected_guests:
|
| 117 |
+
return jsonify({'success': False, 'error': 'No guests selected for arrangement'})
|
| 118 |
+
|
| 119 |
+
# Smart arrangement algorithm - create unlimited tables of 10
|
| 120 |
+
tables = arrange_guests_into_tables(selected_guests)
|
| 121 |
+
|
| 122 |
+
return jsonify({'success': True, 'tables': tables})
|
| 123 |
+
|
| 124 |
+
def arrange_guests_into_tables(guests):
|
| 125 |
+
"""Arrange guests into tables of 10 people each with smart distribution"""
|
| 126 |
+
if not guests:
|
| 127 |
+
return []
|
| 128 |
+
|
| 129 |
+
# Categorize guests by industry/role type based on message content
|
| 130 |
+
categories = {
|
| 131 |
+
'tech': ['engineer', 'developer', 'programmer', 'software', 'tech', 'it', 'data', 'ai', 'ml', 'technology', 'scientist'],
|
| 132 |
+
'business': ['manager', 'director', 'ceo', 'founder', 'executive', 'business', 'strategy', 'operations', 'consultant', 'product'],
|
| 133 |
+
'creative': ['designer', 'creative', 'marketing', 'content', 'writer', 'artist', 'media', 'communications', 'strategist'],
|
| 134 |
+
'sales': ['sales', 'account', 'client', 'business development', 'partnership', 'account manager'],
|
| 135 |
+
'finance': ['finance', 'accounting', 'investment', 'banking', 'financial', 'analyst', 'cfo'],
|
| 136 |
+
'other': []
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
categorized_guests = {cat: [] for cat in categories.keys()}
|
| 140 |
+
|
| 141 |
+
# Categorize each guest based on their message/description
|
| 142 |
+
for guest in guests:
|
| 143 |
+
title_lower = guest['title'].lower()
|
| 144 |
+
categorized = False
|
| 145 |
+
|
| 146 |
+
for category, keywords in categories.items():
|
| 147 |
+
if category == 'other':
|
| 148 |
+
continue
|
| 149 |
+
if any(keyword in title_lower for keyword in keywords):
|
| 150 |
+
categorized_guests[category].append(guest)
|
| 151 |
+
categorized = True
|
| 152 |
+
break
|
| 153 |
+
|
| 154 |
+
if not categorized:
|
| 155 |
+
categorized_guests['other'].append(guest)
|
| 156 |
+
|
| 157 |
+
# Calculate how many tables we need
|
| 158 |
+
total_guests = len(guests)
|
| 159 |
+
num_tables = (total_guests + 9) // 10 # Ceiling division to get number of tables needed
|
| 160 |
+
|
| 161 |
+
# Initialize tables
|
| 162 |
+
tables = [[] for _ in range(num_tables)]
|
| 163 |
+
|
| 164 |
+
# Distribute guests strategically across all tables
|
| 165 |
+
# First, distribute major categories evenly
|
| 166 |
+
major_categories = ['tech', 'business', 'creative', 'sales']
|
| 167 |
+
|
| 168 |
+
for category in major_categories:
|
| 169 |
+
guests_in_category = categorized_guests[category]
|
| 170 |
+
if guests_in_category:
|
| 171 |
+
# Distribute evenly across all tables
|
| 172 |
+
for i, guest in enumerate(guests_in_category):
|
| 173 |
+
table_index = i % num_tables
|
| 174 |
+
if len(tables[table_index]) < 10:
|
| 175 |
+
tables[table_index].append(guest)
|
| 176 |
+
|
| 177 |
+
# Then distribute remaining guests (finance, other)
|
| 178 |
+
remaining_guests = []
|
| 179 |
+
for category in ['finance', 'other']:
|
| 180 |
+
remaining_guests.extend(categorized_guests[category])
|
| 181 |
+
|
| 182 |
+
# Also add any guests that didn't fit in the first round
|
| 183 |
+
for table in tables:
|
| 184 |
+
for guest in guests:
|
| 185 |
+
if guest not in [g for table_guests in tables for g in table_guests]:
|
| 186 |
+
if len(table) < 10:
|
| 187 |
+
table.append(guest)
|
| 188 |
+
break
|
| 189 |
+
|
| 190 |
+
# Fill remaining slots with any leftover guests
|
| 191 |
+
for guest in guests:
|
| 192 |
+
if guest not in [g for table_guests in tables for g in table_guests]:
|
| 193 |
+
for table in tables:
|
| 194 |
+
if len(table) < 10:
|
| 195 |
+
table.append(guest)
|
| 196 |
+
break
|
| 197 |
+
|
| 198 |
+
return tables
|
| 199 |
+
|
| 200 |
+
if __name__ == '__main__':
|
| 201 |
+
app.run(host='0.0.0.0', port=5000)
|
codecommit-policy.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"Version": "2012-10-17",
|
| 3 |
+
"Statement": [
|
| 4 |
+
{
|
| 5 |
+
"Effect": "Allow",
|
| 6 |
+
"Action": [
|
| 7 |
+
"codecommit:ListRepositories",
|
| 8 |
+
"codecommit:GetRepository",
|
| 9 |
+
"codecommit:CreateRepository",
|
| 10 |
+
"codecommit:DeleteRepository",
|
| 11 |
+
"codecommit:GetBranch",
|
| 12 |
+
"codecommit:CreateBranch",
|
| 13 |
+
"codecommit:DeleteBranch",
|
| 14 |
+
"codecommit:ListBranches",
|
| 15 |
+
"codecommit:GetCommit",
|
| 16 |
+
"codecommit:GetDifferences",
|
| 17 |
+
"codecommit:GetFile",
|
| 18 |
+
"codecommit:GetFolder",
|
| 19 |
+
"codecommit:PutFile",
|
| 20 |
+
"codecommit:CreateCommit",
|
| 21 |
+
"codecommit:BatchGetRepositories",
|
| 22 |
+
"codecommit:GetBlob",
|
| 23 |
+
"codecommit:GetTree",
|
| 24 |
+
"codecommit:GetCommitHistory",
|
| 25 |
+
"codecommit:GetPullRequest",
|
| 26 |
+
"codecommit:ListPullRequests",
|
| 27 |
+
"codecommit:CreatePullRequest",
|
| 28 |
+
"codecommit:MergePullRequestByFastForward",
|
| 29 |
+
"codecommit:PostCommentForPullRequest",
|
| 30 |
+
"codecommit:GetCommentsForPullRequest",
|
| 31 |
+
"codecommit:GetCommentsForComparedCommit",
|
| 32 |
+
"codecommit:PostCommentReply",
|
| 33 |
+
"codecommit:GetComment",
|
| 34 |
+
"codecommit:UpdateComment",
|
| 35 |
+
"codecommit:DeleteComment",
|
| 36 |
+
"codecommit:GetMergeConflicts",
|
| 37 |
+
"codecommit:DescribeMergeConflicts",
|
| 38 |
+
"codecommit:GetMergeOptions",
|
| 39 |
+
"codecommit:BatchDescribeMergeConflicts",
|
| 40 |
+
"codecommit:EvaluatePullRequestApprovalRules",
|
| 41 |
+
"codecommit:OverridePullRequestApprovalRules",
|
| 42 |
+
"codecommit:GetApprovalRuleTemplate",
|
| 43 |
+
"codecommit:ListApprovalRuleTemplates",
|
| 44 |
+
"codecommit:CreateApprovalRuleTemplate",
|
| 45 |
+
"codecommit:UpdateApprovalRuleTemplateName",
|
| 46 |
+
"codecommit:UpdateApprovalRuleTemplateDescription",
|
| 47 |
+
"codecommit:UpdateApprovalRuleTemplateContent",
|
| 48 |
+
"codecommit:DeleteApprovalRuleTemplate",
|
| 49 |
+
"codecommit:ListRepositoriesForApprovalRuleTemplate",
|
| 50 |
+
"codecommit:GetRepositoryTriggers",
|
| 51 |
+
"codecommit:PutRepositoryTriggers",
|
| 52 |
+
"codecommit:TestRepositoryTriggers",
|
| 53 |
+
"codecommit:GetRepositoryPolicy",
|
| 54 |
+
"codecommit:SetRepositoryPolicy",
|
| 55 |
+
"codecommit:DeleteRepositoryPolicy",
|
| 56 |
+
"codecommit:GetRepositoryTriggers",
|
| 57 |
+
"codecommit:PutRepositoryTriggers",
|
| 58 |
+
"codecommit:TestRepositoryTriggers"
|
| 59 |
+
],
|
| 60 |
+
"Resource": "*"
|
| 61 |
+
}
|
| 62 |
+
]
|
| 63 |
+
}
|
data/guest_list.csv
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Name,Description
|
| 2 |
+
John Park,Pleasure talking to you at AGI house sf today. Best wishes on your future plans and wishing good health to you and your wife and baby in Brazil.
|
| 3 |
+
Adam Wong,"Adam, inspiring to hear you speak at the War in Space gathering at 1515 Folsom this evening. Keep up the great work! Dan "
|
| 4 |
+
Alice Oh,Pleasure listening to your presentation at Adobe's AEP GenAI Seminar today.
|
| 5 |
+
Dustin Calim,
|
| 6 |
+
Dhruv Diddi ⚡,
|
| 7 |
+
Daniel Huang,
|
| 8 |
+
Sean Mudge,"Sean, pleasure meeting you at today’s robotics hackathon. Hope to meet up again since we’re both in the San Jose area. Dan"
|
| 9 |
+
Arash Tajik,
|
| 10 |
+
Sharveen Kumar L,
|
| 11 |
+
Roman Gurovich,
|
| 12 |
+
Nicholas Broad,
|
| 13 |
+
Varin Nair,Pleasure talking with you this evening. Thanks for enlightening me about Factory.
|
| 14 |
+
Eno Reyes,"Eno, Happy to chat with at tonight’s event. Dan"
|
| 15 |
+
Michelle Shen,"Pleasure being the first to the Semianalysis gathering tonight so I could learn more about the organization through you. Hope you had a good evening and best wishes, Dan"
|
| 16 |
+
Shengtan Mao,
|
| 17 |
+
William Chen,
|
| 18 |
+
Aliya Ismagilova,Pleasure meeting you at the Modular hackathon. Best wishes at John Hopkins Physics.
|
| 19 |
+
Nikki Solanki,
|
| 20 |
+
Mohamed Fawzy,Pleasure listening to your insights on training infrastructure at today’s panel discussion at Meta. Hope to catch you at the table talks.
|
| 21 |
+
Hatice Ozen,Pleasure talking to you today about all things Saudi Arabia and Groq. Followed you back on X.
|
| 22 |
+
Div Garg,"Div, Pleasure connecting with you at AGI House tonight. Happy to talk more and best wishes on your research spin-off. Dan"
|
| 23 |
+
Connor Hack,
|
| 24 |
+
Zoe Yan,Pleasure meeting you at the LlamaCon hackathon
|
| 25 |
+
Junseok Oh,
|
| 26 |
+
Kaixing (Kai) Wu,Pleasure meeting you at the LlamaCon hackathon
|
| 27 |
+
"Alan ""Bolo"" Bologlu",Pleasure knowing about you through the article in sfgate today. Sent you a message through your site re: interest in the properties as a collector. Cheers!
|
| 28 |
+
Raymond Liao,
|
| 29 |
+
James Hennessy,"Hi James, we met at the Strong Compute GPU hackathon about a month ago where you presented and we had also talked about a CUDA LLM translation project. Would love to connect. - Dan"
|
| 30 |
+
Maggie Gray,Thanks for sponsoring the NatSec hackathon this past weekend. It was a great event.
|
| 31 |
+
Steven Sloss,
|
| 32 |
+
Isaac Struhl,Fun chatting with you while figuring out the Distributed Spectrum puzzle at Shack15.
|
| 33 |
+
Noah Olsen⚡🦅,
|
| 34 |
+
Daniel Moore,
|
| 35 |
+
Walter Woo,
|
| 36 |
+
Lakshya lnu,Pleasure hitting with you in vball and talking about foundational models.
|
| 37 |
+
Maria Yap,
|
| 38 |
+
Aran Khanna,Pleasure meeting you at the Amplify dinner this evening. Best wishes - Dan
|
| 39 |
+
Shinji Kim,
|
| 40 |
+
Jack Rae,"Jack, really enjoyed your presentation on Friday at AGI House expounding on the state of Thinking models. Best wishes - Dan"
|
| 41 |
+
Karthik Ragunath Ananda Kumar,Algol to smart split video
|
| 42 |
+
Chris Perry,"Hi Chris, I was referred to you by Paige during the Deepmind talks this evening at AGI House. Encountered an error following her demos of the DataScience collab with Gemini and she referred me to you. Best wishes - Dan"
|
| 43 |
+
Paige Bailey,Hi Paige. I just showed you the error on Data Science Collab. Thanks for the talks and the demos - they were very informative. Will send you more details later. - Dan
|
| 44 |
+
Simon Wex,Pleasure chatting with you and ruminating about the future of software development and everything else.
|
| 45 |
+
Ravi Kiran Chirravuri,"Pleasure talking to you at the Roblox mixer this evening. Best wishes, Dan"
|
| 46 |
+
Santosh Alex,Pleasure meeting at the Roblox mixer
|
| 47 |
+
Foad Dabiri,Thanks for enlightening me about the economy group at Roblox. Best wishes - Dan
|
| 48 |
+
Brian Su,Pleasure talking you at the Roblox mixer this evening. Best wishes - Dan
|
| 49 |
+
Chang Xu,Pleasure meeting you at the Roblox mixer. Thanks for elaborating on what you do. - Dan
|
| 50 |
+
"Alireza Boloorchi, PhD",
|
| 51 |
+
Alan Yu,"Alan, I’m an angel investor in the outer space area and would love to connect and share deal flow. I’m going to refer you to Astral Materials who is looking to raise. FYI, I heard about you through Oddbird VC. Cheers, Dan"
|
| 52 |
+
Ashkan Mizani,
|
| 53 |
+
Wayne Crosby,"Wayne, pleasure listening to you this evening and answering my questions around data. Best wishes, Dan"
|
| 54 |
+
Jiya Janowitz,Pleasure meeting you at SSGL at Twin Peaks on Sunday. Best wishes at Astral Materials.
|
| 55 |
+
Jessica Frick,"Hi Jessica, Happy to have met you at Alder's SSGL meetup at Twin Peaks on Sunday and learned about Astral Materials. Please send me a pitch dec at your convenience. Best wishes - Dan"
|
| 56 |
+
Yury Sokolov,
|
| 57 |
+
Jono Ridgway,
|
| 58 |
+
Chase Denecke,Great pleasure talking to you at the Space Meetup at 200 Palo Alto. Fascinating to hear the ways you’re working with stem cells and embryos. Best wishes on your startup . Dan
|
| 59 |
+
Jeffrey Morgan,
|
| 60 |
+
Grace Brown,Pleasure meeting you at the Ollama event at YC
|
| 61 |
+
Woosuk Kwon,Looking forward to seeing you at the meetup at YC
|
| 62 |
+
Michelle Oh,"Pleasure connecting with you this evening- would definitely be interested in your next funding round, assuming you haven’t already blown up after the a16z event. Best wishes. Dan"
|
| 63 |
+
Hamza Adil,
|
| 64 |
+
Sophia Aryan,"Sophia, Pleasure attending you and BuzzRobot’s interview of Peter Norvig this evening. Hope to attend more events in the future. Dan"
|
| 65 |
+
Tingyu Zhang,
|
| 66 |
+
Rahman Hajiyev,
|
| 67 |
+
Burkay Gur,
|
| 68 |
+
Driss Guessous,Pleasure talking to you at Beyond CUDA today. Best wishes at Meta LA. Dan
|
| 69 |
+
Eugene Cheah,"Eugene, Pleasure listening to you at today’s hot takes discussion. Dan"
|
| 70 |
+
Alex Zhang,"Alex, pleasure hearing you talk this evening at gpu mode and hearing your opinions on TK vs cutlass. Best wishes, Dan"
|
| 71 |
+
Lantao Yu,
|
| 72 |
+
Brian Hirsh,
|
| 73 |
+
Eddie Yan,
|
| 74 |
+
Jeffrey Wang,Pleasure listening to your talk today at AGI House SF
|
| 75 |
+
Jennifer Smith,Pleasure talking to you at FYSK this evening about Scribe and the EM positions. Best wishes. Dan
|
| 76 |
+
Melisa Tokmak,
|
| 77 |
+
Walden Yan,
|
| 78 |
+
Chris Yeh,Thanks for the book signing this morning ( I was the last signing that brought his own book ) at AGI House and the very valuable workshop. Re: being the emotional crutch for solopreneurs not being scalable - I guess it’s ai agents to the rescue again!
|
| 79 |
+
Nuno Campos,"Nuno, pleasure meeting you at AGI House today."
|
| 80 |
+
Zade Ismael,
|
| 81 |
+
Ryan Scott,
|
| 82 |
+
Kurtis Evan David,
|
| 83 |
+
Marius Seritan,
|
| 84 |
+
Judy Abad,
|
| 85 |
+
Quinn Li O'Shea,
|
| 86 |
+
Ashley Fieglein Johnson,
|
| 87 |
+
James Le,"James, I just participated in the multimodal hackathon at Weights and Biases and saw your demo with ApertureData re: Video Semantic Search. Happy to connect and hope to chat IRL when TwelveLabs has an event coming up in the Bay Area. Cheers! Dan"
|
| 88 |
+
Valentina Todeschi ,
|
| 89 |
+
Shail Highbloom,
|
| 90 |
+
Aman Gour,
|
| 91 |
+
Nan Xia,
|
| 92 |
+
Victor Wang,
|
| 93 |
+
Aidin Khosrowshahi,
|
| 94 |
+
Rahul Tarak,
|
| 95 |
+
"Joanna Zhang SE, PE, PMP, LEED AP",
|
| 96 |
+
Will Yin,
|
| 97 |
+
Eric Arnoldy,
|
| 98 |
+
Lynne Guey,
|
| 99 |
+
Michael Pierantozzi,
|
| 100 |
+
Mark Saroufim,"Mark, pleasure meeting you at the Nvidia meetup this evening and talking about Cudamode. Best wishes, Dan"
|
| 101 |
+
Rachel Jordan,Pleasure meeting you at the Nvidia CUDA meetup
|
| 102 |
+
Anshuman Bhat,
|
| 103 |
+
Jayram A. Deshpande,Nvidia meetup. Space logistics
|
| 104 |
+
Jake Hemstad,
|
| 105 |
+
Holden Karau,
|
| 106 |
+
Ashish Thusoo,"Hi Ashish, just replied to your email. Would love to connect. Cheers - Dan"
|
| 107 |
+
Nathan Zhao,
|
| 108 |
+
Jung Moon,
|
| 109 |
+
Nick Pezzotti,
|
| 110 |
+
Gary Yao,
|
| 111 |
+
Clovis Vinant-Tang,
|
| 112 |
+
Zack Li,
|
| 113 |
+
Kyle Stratis,Pleasure meeting you at Chip's virtual get together. Best wishes on your consulting practice.
|
| 114 |
+
Leonard Lee,
|
| 115 |
+
"Fei (Kelvin) Chu, MS, MBA",
|
| 116 |
+
"Jack Luo, MD",
|
| 117 |
+
🪡 Sam Stowers,
|
| 118 |
+
Daniel Han-Chen,
|
| 119 |
+
Yvonne Lung,"Hi Yvonne, happy to honor your friend request on FB. Cool that Lone Moon is investing in Dallas, TX. I'm also in a syndicate that invests in multifamily apartments in Dallas. Cheers and Happy New Year! Dan"
|
| 120 |
+
Brandon Wang,
|
| 121 |
+
shahin.farshchi@luxcapital.com,"Hi Shahin, enjoyed your answers at the panel discussion in the Alumni Ventures offices a few weeks ago. Wanted to chat about outer space companies, but you had to leave early. Best wishes, Dan"
|
| 122 |
+
Vitaly Bulatov,
|
requirements.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask==3.0.0
|
| 2 |
+
flask-cors==4.0.0
|
| 3 |
+
werkzeug==3.1.3
|
| 4 |
+
gunicorn==21.2.0
|
requirements_hf.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.0.0
|
run.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
LinkedIn Party Planner - Startup Script
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import sys
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
def check_dependencies():
|
| 10 |
+
"""Check if required dependencies are installed"""
|
| 11 |
+
try:
|
| 12 |
+
import flask
|
| 13 |
+
import selenium
|
| 14 |
+
import webdriver_manager
|
| 15 |
+
print("✅ All dependencies are installed")
|
| 16 |
+
return True
|
| 17 |
+
except ImportError as e:
|
| 18 |
+
print(f"❌ Missing dependency: {e}")
|
| 19 |
+
print("Please run: pip install -r requirements.txt")
|
| 20 |
+
return False
|
| 21 |
+
|
| 22 |
+
def check_chrome():
|
| 23 |
+
"""Check if Chrome browser is available"""
|
| 24 |
+
import subprocess
|
| 25 |
+
try:
|
| 26 |
+
# Try to find Chrome on different platforms
|
| 27 |
+
chrome_paths = [
|
| 28 |
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", # macOS
|
| 29 |
+
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", # Windows
|
| 30 |
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", # Windows 32-bit
|
| 31 |
+
"/usr/bin/google-chrome", # Linux
|
| 32 |
+
"/usr/bin/chromium-browser" # Linux Chromium
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
for path in chrome_paths:
|
| 36 |
+
if os.path.exists(path):
|
| 37 |
+
print("✅ Chrome browser found")
|
| 38 |
+
return True
|
| 39 |
+
|
| 40 |
+
# Try using 'which' command
|
| 41 |
+
result = subprocess.run(['which', 'google-chrome'], capture_output=True, text=True)
|
| 42 |
+
if result.returncode == 0:
|
| 43 |
+
print("✅ Chrome browser found")
|
| 44 |
+
return True
|
| 45 |
+
|
| 46 |
+
print("⚠️ Chrome browser not found in common locations")
|
| 47 |
+
print("Please make sure Chrome is installed and accessible")
|
| 48 |
+
return False
|
| 49 |
+
|
| 50 |
+
except Exception as e:
|
| 51 |
+
print(f"⚠️ Could not verify Chrome installation: {e}")
|
| 52 |
+
return True # Assume it's available
|
| 53 |
+
|
| 54 |
+
def main():
|
| 55 |
+
"""Main startup function"""
|
| 56 |
+
print("🚀 LinkedIn Party Planner")
|
| 57 |
+
print("=" * 40)
|
| 58 |
+
|
| 59 |
+
# Check dependencies
|
| 60 |
+
if not check_dependencies():
|
| 61 |
+
sys.exit(1)
|
| 62 |
+
|
| 63 |
+
# Check Chrome
|
| 64 |
+
check_chrome()
|
| 65 |
+
|
| 66 |
+
print("\n📋 Starting the application...")
|
| 67 |
+
print("🌐 The app will be available at: http://localhost:5000")
|
| 68 |
+
print("📖 For help, see README.md")
|
| 69 |
+
print("\n" + "=" * 40)
|
| 70 |
+
|
| 71 |
+
# Import and run the app
|
| 72 |
+
try:
|
| 73 |
+
from app import app
|
| 74 |
+
app.run(debug=True, host='0.0.0.0', port=5000)
|
| 75 |
+
except KeyboardInterrupt:
|
| 76 |
+
print("\n👋 Application stopped by user")
|
| 77 |
+
except Exception as e:
|
| 78 |
+
print(f"\n❌ Error starting application: {e}")
|
| 79 |
+
sys.exit(1)
|
| 80 |
+
|
| 81 |
+
if __name__ == "__main__":
|
| 82 |
+
main()
|
sample_guests.csv
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name,message
|
| 2 |
+
John Smith,Software Engineer at TechCorp
|
| 3 |
+
Sarah Johnson,Marketing Director at Creative Agency
|
| 4 |
+
Michael Brown,CEO of StartupXYZ
|
| 5 |
+
Emily Davis,Data Scientist at AI Labs
|
| 6 |
+
David Wilson,Product Manager at Innovation Inc
|
| 7 |
+
Lisa Anderson,Sales Director at Growth Co
|
| 8 |
+
Robert Taylor,UX Designer at Design Studio
|
| 9 |
+
Jennifer Martinez,Finance Manager at Finance Corp
|
| 10 |
+
Christopher Lee,Operations Director at Logistics Ltd
|
| 11 |
+
Amanda White,Content Strategist at Media Group
|
templates/index.html
ADDED
|
@@ -0,0 +1,779 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Party Planner - Guest Table Arranger</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
body {
|
| 15 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
| 16 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 17 |
+
min-height: 100vh;
|
| 18 |
+
color: #333;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.container {
|
| 22 |
+
max-width: 1200px;
|
| 23 |
+
margin: 0 auto;
|
| 24 |
+
padding: 20px;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.header {
|
| 28 |
+
text-align: center;
|
| 29 |
+
color: white;
|
| 30 |
+
margin-bottom: 40px;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
.header h1 {
|
| 34 |
+
font-size: 2.5rem;
|
| 35 |
+
margin-bottom: 10px;
|
| 36 |
+
font-weight: 300;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
.header p {
|
| 40 |
+
font-size: 1.1rem;
|
| 41 |
+
opacity: 0.9;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.card {
|
| 45 |
+
background: white;
|
| 46 |
+
border-radius: 12px;
|
| 47 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
| 48 |
+
padding: 30px;
|
| 49 |
+
margin-bottom: 30px;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
.upload-form {
|
| 53 |
+
max-width: 500px;
|
| 54 |
+
margin: 0 auto;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
.form-group {
|
| 58 |
+
margin-bottom: 20px;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.form-group label {
|
| 62 |
+
display: block;
|
| 63 |
+
margin-bottom: 8px;
|
| 64 |
+
font-weight: 500;
|
| 65 |
+
color: #333;
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.file-upload-container {
|
| 69 |
+
border: 2px dashed #667eea;
|
| 70 |
+
border-radius: 8px;
|
| 71 |
+
padding: 40px 20px;
|
| 72 |
+
text-align: center;
|
| 73 |
+
background: #f8f9ff;
|
| 74 |
+
transition: all 0.3s ease;
|
| 75 |
+
cursor: pointer;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.file-upload-container:hover {
|
| 79 |
+
border-color: #5a6fd8;
|
| 80 |
+
background: #f0f2ff;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.file-upload-container.dragover {
|
| 84 |
+
border-color: #5a6fd8;
|
| 85 |
+
background: #e8ecff;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.file-upload-icon {
|
| 89 |
+
font-size: 3rem;
|
| 90 |
+
color: #667eea;
|
| 91 |
+
margin-bottom: 15px;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.file-upload-text {
|
| 95 |
+
font-size: 1.1rem;
|
| 96 |
+
color: #667eea;
|
| 97 |
+
margin-bottom: 10px;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.file-upload-hint {
|
| 101 |
+
font-size: 0.9rem;
|
| 102 |
+
color: #666;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.file-input {
|
| 106 |
+
display: none;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.btn {
|
| 110 |
+
background: #667eea;
|
| 111 |
+
color: white;
|
| 112 |
+
border: none;
|
| 113 |
+
padding: 12px 24px;
|
| 114 |
+
border-radius: 8px;
|
| 115 |
+
font-size: 16px;
|
| 116 |
+
font-weight: 500;
|
| 117 |
+
cursor: pointer;
|
| 118 |
+
transition: background-color 0.3s ease;
|
| 119 |
+
width: 100%;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.btn:hover {
|
| 123 |
+
background: #5a6fd8;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.btn:disabled {
|
| 127 |
+
background: #ccc;
|
| 128 |
+
cursor: not-allowed;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.btn-secondary {
|
| 132 |
+
background: #6c757d;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.btn-secondary:hover {
|
| 136 |
+
background: #545b62;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
.btn-success {
|
| 140 |
+
background: #28a745;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.btn-success:hover {
|
| 144 |
+
background: #1e7e34;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.guests-grid {
|
| 148 |
+
display: grid;
|
| 149 |
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
| 150 |
+
gap: 20px;
|
| 151 |
+
margin-bottom: 30px;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.guest-card {
|
| 155 |
+
border: 2px solid #e1e5e9;
|
| 156 |
+
border-radius: 8px;
|
| 157 |
+
padding: 20px;
|
| 158 |
+
transition: all 0.3s ease;
|
| 159 |
+
cursor: pointer;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.guest-card:hover {
|
| 163 |
+
border-color: #667eea;
|
| 164 |
+
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.guest-card.selected {
|
| 168 |
+
border-color: #667eea;
|
| 169 |
+
background: #f8f9ff;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.guest-checkbox {
|
| 173 |
+
margin-right: 12px;
|
| 174 |
+
transform: scale(1.2);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.guest-name {
|
| 178 |
+
font-weight: 600;
|
| 179 |
+
font-size: 1.1rem;
|
| 180 |
+
margin-bottom: 8px;
|
| 181 |
+
color: #667eea;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.guest-title {
|
| 185 |
+
color: #666;
|
| 186 |
+
font-size: 0.9rem;
|
| 187 |
+
line-height: 1.4;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.tables-container {
|
| 191 |
+
display: grid;
|
| 192 |
+
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
| 193 |
+
gap: 40px;
|
| 194 |
+
margin-top: 30px;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.table-card {
|
| 198 |
+
background: #f8f9fa;
|
| 199 |
+
border-radius: 12px;
|
| 200 |
+
padding: 25px;
|
| 201 |
+
border: 2px solid #e1e5e9;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.table-header {
|
| 205 |
+
text-align: center;
|
| 206 |
+
margin-bottom: 30px;
|
| 207 |
+
padding-bottom: 15px;
|
| 208 |
+
border-bottom: 2px solid #667eea;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.table-title {
|
| 212 |
+
font-size: 1.3rem;
|
| 213 |
+
font-weight: 600;
|
| 214 |
+
color: #667eea;
|
| 215 |
+
margin-bottom: 5px;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.table-count {
|
| 219 |
+
color: #666;
|
| 220 |
+
font-size: 0.9rem;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.circular-table {
|
| 224 |
+
position: relative;
|
| 225 |
+
width: 300px;
|
| 226 |
+
height: 300px;
|
| 227 |
+
margin: 0 auto 30px;
|
| 228 |
+
border-radius: 50%;
|
| 229 |
+
background: linear-gradient(145deg, #e6e6e6, #ffffff);
|
| 230 |
+
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
| 231 |
+
border: 3px solid #667eea;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.table-center {
|
| 235 |
+
position: absolute;
|
| 236 |
+
top: 50%;
|
| 237 |
+
left: 50%;
|
| 238 |
+
transform: translate(-50%, -50%);
|
| 239 |
+
width: 60px;
|
| 240 |
+
height: 60px;
|
| 241 |
+
background: #667eea;
|
| 242 |
+
border-radius: 50%;
|
| 243 |
+
display: flex;
|
| 244 |
+
align-items: center;
|
| 245 |
+
justify-content: center;
|
| 246 |
+
color: white;
|
| 247 |
+
font-weight: 600;
|
| 248 |
+
font-size: 0.9rem;
|
| 249 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.seat {
|
| 253 |
+
position: absolute;
|
| 254 |
+
width: 50px;
|
| 255 |
+
height: 50px;
|
| 256 |
+
background: #28a745;
|
| 257 |
+
border-radius: 50%;
|
| 258 |
+
display: flex;
|
| 259 |
+
align-items: center;
|
| 260 |
+
justify-content: center;
|
| 261 |
+
color: white;
|
| 262 |
+
font-size: 0.7rem;
|
| 263 |
+
font-weight: 600;
|
| 264 |
+
text-align: center;
|
| 265 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
| 266 |
+
transition: all 0.3s ease;
|
| 267 |
+
cursor: pointer;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.seat:hover {
|
| 271 |
+
transform: scale(1.1);
|
| 272 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.seat-info {
|
| 276 |
+
position: absolute;
|
| 277 |
+
bottom: -60px;
|
| 278 |
+
left: 50%;
|
| 279 |
+
transform: translateX(-50%);
|
| 280 |
+
background: #333;
|
| 281 |
+
color: white;
|
| 282 |
+
padding: 8px 12px;
|
| 283 |
+
border-radius: 6px;
|
| 284 |
+
font-size: 0.8rem;
|
| 285 |
+
white-space: nowrap;
|
| 286 |
+
opacity: 0;
|
| 287 |
+
transition: opacity 0.3s ease;
|
| 288 |
+
pointer-events: none;
|
| 289 |
+
z-index: 10;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
.seat-info::before {
|
| 293 |
+
content: '';
|
| 294 |
+
position: absolute;
|
| 295 |
+
top: -5px;
|
| 296 |
+
left: 50%;
|
| 297 |
+
transform: translateX(-50%);
|
| 298 |
+
border-left: 5px solid transparent;
|
| 299 |
+
border-right: 5px solid transparent;
|
| 300 |
+
border-bottom: 5px solid #333;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.seat:hover .seat-info {
|
| 304 |
+
opacity: 1;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
.stats-panel {
|
| 308 |
+
background: #667eea;
|
| 309 |
+
color: white;
|
| 310 |
+
padding: 20px;
|
| 311 |
+
border-radius: 12px;
|
| 312 |
+
margin-bottom: 30px;
|
| 313 |
+
text-align: center;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.stats-grid {
|
| 317 |
+
display: grid;
|
| 318 |
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 319 |
+
gap: 20px;
|
| 320 |
+
margin-top: 15px;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
.stat-item {
|
| 324 |
+
text-align: center;
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
.stat-number {
|
| 328 |
+
font-size: 2rem;
|
| 329 |
+
font-weight: 600;
|
| 330 |
+
margin-bottom: 5px;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.stat-label {
|
| 334 |
+
font-size: 0.9rem;
|
| 335 |
+
opacity: 0.9;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.loading {
|
| 339 |
+
text-align: center;
|
| 340 |
+
padding: 40px;
|
| 341 |
+
color: #666;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.spinner {
|
| 345 |
+
border: 4px solid #f3f3f3;
|
| 346 |
+
border-top: 4px solid #667eea;
|
| 347 |
+
border-radius: 50%;
|
| 348 |
+
width: 40px;
|
| 349 |
+
height: 40px;
|
| 350 |
+
animation: spin 1s linear infinite;
|
| 351 |
+
margin: 0 auto 20px;
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
@keyframes spin {
|
| 355 |
+
0% { transform: rotate(0deg); }
|
| 356 |
+
100% { transform: rotate(360deg); }
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.error-message {
|
| 360 |
+
background: #f8d7da;
|
| 361 |
+
color: #721c24;
|
| 362 |
+
padding: 15px;
|
| 363 |
+
border-radius: 8px;
|
| 364 |
+
margin-bottom: 20px;
|
| 365 |
+
border: 1px solid #f5c6cb;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.success-message {
|
| 369 |
+
background: #d4edda;
|
| 370 |
+
color: #155724;
|
| 371 |
+
padding: 15px;
|
| 372 |
+
border-radius: 8px;
|
| 373 |
+
margin-bottom: 20px;
|
| 374 |
+
border: 1px solid #c3e6cb;
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.csv-format-info {
|
| 378 |
+
background: #e7f3ff;
|
| 379 |
+
border: 1px solid #b3d9ff;
|
| 380 |
+
border-radius: 8px;
|
| 381 |
+
padding: 20px;
|
| 382 |
+
margin-bottom: 20px;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.csv-format-info h3 {
|
| 386 |
+
color: #0056b3;
|
| 387 |
+
margin-bottom: 10px;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
.csv-format-info ul {
|
| 391 |
+
margin-left: 20px;
|
| 392 |
+
color: #333;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.csv-format-info li {
|
| 396 |
+
margin-bottom: 5px;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
.hidden {
|
| 400 |
+
display: none;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
@media (max-width: 768px) {
|
| 404 |
+
.container {
|
| 405 |
+
padding: 10px;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
.header h1 {
|
| 409 |
+
font-size: 2rem;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.guests-grid {
|
| 413 |
+
grid-template-columns: 1fr;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.tables-container {
|
| 417 |
+
grid-template-columns: 1fr;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
.circular-table {
|
| 421 |
+
width: 250px;
|
| 422 |
+
height: 250px;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.seat {
|
| 426 |
+
width: 40px;
|
| 427 |
+
height: 40px;
|
| 428 |
+
font-size: 0.6rem;
|
| 429 |
+
}
|
| 430 |
+
}
|
| 431 |
+
</style>
|
| 432 |
+
</head>
|
| 433 |
+
<body>
|
| 434 |
+
<div class="container">
|
| 435 |
+
<div class="header">
|
| 436 |
+
<h1>Party Planner</h1>
|
| 437 |
+
<p>Upload your guest list and arrange them into perfect networking tables</p>
|
| 438 |
+
</div>
|
| 439 |
+
|
| 440 |
+
<!-- Upload Section -->
|
| 441 |
+
<div class="card" id="uploadSection">
|
| 442 |
+
<div class="upload-form">
|
| 443 |
+
<div class="csv-format-info">
|
| 444 |
+
<h3>📋 CSV Format Requirements</h3>
|
| 445 |
+
<ul>
|
| 446 |
+
<li>Your CSV should have exactly 2 columns: name and message</li>
|
| 447 |
+
<li>The first column should contain guest names</li>
|
| 448 |
+
<li>The second column should contain descriptions, roles, or messages</li>
|
| 449 |
+
<li>Entries with blank names or descriptions will be automatically filtered out</li>
|
| 450 |
+
<li>No limit on the number of guests - tables will be created as needed</li>
|
| 451 |
+
</ul>
|
| 452 |
+
</div>
|
| 453 |
+
|
| 454 |
+
<div class="form-group">
|
| 455 |
+
<div class="file-upload-container" id="fileUploadContainer">
|
| 456 |
+
<div class="file-upload-icon">📁</div>
|
| 457 |
+
<div class="file-upload-text">Click to upload or drag and drop</div>
|
| 458 |
+
<div class="file-upload-hint">CSV files only</div>
|
| 459 |
+
<input type="file" id="csvFile" class="file-input" accept=".csv" />
|
| 460 |
+
</div>
|
| 461 |
+
</div>
|
| 462 |
+
|
| 463 |
+
<button class="btn" id="uploadBtn" disabled>Upload Guest List</button>
|
| 464 |
+
</div>
|
| 465 |
+
</div>
|
| 466 |
+
|
| 467 |
+
<!-- Loading Section -->
|
| 468 |
+
<div class="card hidden" id="loadingSection">
|
| 469 |
+
<div class="loading">
|
| 470 |
+
<div class="spinner"></div>
|
| 471 |
+
<p>Processing your guest list...</p>
|
| 472 |
+
</div>
|
| 473 |
+
</div>
|
| 474 |
+
|
| 475 |
+
<!-- Guest Selection Section -->
|
| 476 |
+
<div class="card hidden" id="guestSection">
|
| 477 |
+
<h2 style="margin-bottom: 20px; color: #667eea;">Select Guests for Your Event</h2>
|
| 478 |
+
|
| 479 |
+
<div class="stats-panel">
|
| 480 |
+
<div class="stats-grid">
|
| 481 |
+
<div class="stat-item">
|
| 482 |
+
<div class="stat-number" id="totalGuests">0</div>
|
| 483 |
+
<div class="stat-label">Total Guests</div>
|
| 484 |
+
</div>
|
| 485 |
+
<div class="stat-item">
|
| 486 |
+
<div class="stat-number" id="selectedGuests">0</div>
|
| 487 |
+
<div class="stat-label">Selected</div>
|
| 488 |
+
</div>
|
| 489 |
+
<div class="stat-item">
|
| 490 |
+
<div class="stat-number" id="remainingSlots">0</div>
|
| 491 |
+
<div class="stat-label">Tables Needed</div>
|
| 492 |
+
</div>
|
| 493 |
+
</div>
|
| 494 |
+
</div>
|
| 495 |
+
|
| 496 |
+
<div style="text-align: center; margin-bottom: 20px;">
|
| 497 |
+
<button class="btn btn-secondary" id="selectAllBtn" style="width: auto; margin-right: 10px;">Select All</button>
|
| 498 |
+
<button class="btn btn-secondary" id="deselectAllBtn" style="width: auto;">Deselect All</button>
|
| 499 |
+
</div>
|
| 500 |
+
|
| 501 |
+
<div class="guests-grid" id="guestsGrid">
|
| 502 |
+
<!-- Guest cards will be populated here -->
|
| 503 |
+
</div>
|
| 504 |
+
|
| 505 |
+
<div style="text-align: center; margin-top: 30px;">
|
| 506 |
+
<button class="btn btn-success" id="arrangeBtn" disabled>Arrange Tables</button>
|
| 507 |
+
</div>
|
| 508 |
+
</div>
|
| 509 |
+
|
| 510 |
+
<!-- Tables Section -->
|
| 511 |
+
<div class="card hidden" id="tablesSection">
|
| 512 |
+
<h2 style="margin-bottom: 20px; color: #667eea;">Your Event Tables</h2>
|
| 513 |
+
|
| 514 |
+
<div class="tables-container" id="tablesContainer">
|
| 515 |
+
<!-- Tables will be populated here -->
|
| 516 |
+
</div>
|
| 517 |
+
|
| 518 |
+
<div style="text-align: center; margin-top: 30px;">
|
| 519 |
+
<button class="btn btn-secondary" id="backToGuestsBtn">Back to Guest Selection</button>
|
| 520 |
+
</div>
|
| 521 |
+
</div>
|
| 522 |
+
</div>
|
| 523 |
+
|
| 524 |
+
<script>
|
| 525 |
+
let guests = [];
|
| 526 |
+
let selectedGuests = [];
|
| 527 |
+
|
| 528 |
+
// File upload handling
|
| 529 |
+
const fileUploadContainer = document.getElementById('fileUploadContainer');
|
| 530 |
+
const csvFileInput = document.getElementById('csvFile');
|
| 531 |
+
const uploadBtn = document.getElementById('uploadBtn');
|
| 532 |
+
|
| 533 |
+
fileUploadContainer.addEventListener('click', () => {
|
| 534 |
+
csvFileInput.click();
|
| 535 |
+
});
|
| 536 |
+
|
| 537 |
+
fileUploadContainer.addEventListener('dragover', (e) => {
|
| 538 |
+
e.preventDefault();
|
| 539 |
+
fileUploadContainer.classList.add('dragover');
|
| 540 |
+
});
|
| 541 |
+
|
| 542 |
+
fileUploadContainer.addEventListener('dragleave', () => {
|
| 543 |
+
fileUploadContainer.classList.remove('dragover');
|
| 544 |
+
});
|
| 545 |
+
|
| 546 |
+
fileUploadContainer.addEventListener('drop', (e) => {
|
| 547 |
+
e.preventDefault();
|
| 548 |
+
fileUploadContainer.classList.remove('dragover');
|
| 549 |
+
|
| 550 |
+
const files = e.dataTransfer.files;
|
| 551 |
+
if (files.length > 0) {
|
| 552 |
+
csvFileInput.files = files;
|
| 553 |
+
handleFileSelect();
|
| 554 |
+
}
|
| 555 |
+
});
|
| 556 |
+
|
| 557 |
+
csvFileInput.addEventListener('change', handleFileSelect);
|
| 558 |
+
|
| 559 |
+
function handleFileSelect() {
|
| 560 |
+
const file = csvFileInput.files[0];
|
| 561 |
+
if (file) {
|
| 562 |
+
uploadBtn.disabled = false;
|
| 563 |
+
fileUploadContainer.querySelector('.file-upload-text').textContent = `Selected: ${file.name}`;
|
| 564 |
+
} else {
|
| 565 |
+
uploadBtn.disabled = true;
|
| 566 |
+
fileUploadContainer.querySelector('.file-upload-text').textContent = 'Click to upload or drag and drop';
|
| 567 |
+
}
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
uploadBtn.addEventListener('click', uploadCSV);
|
| 571 |
+
|
| 572 |
+
async function uploadCSV() {
|
| 573 |
+
const file = csvFileInput.files[0];
|
| 574 |
+
if (!file) return;
|
| 575 |
+
|
| 576 |
+
const formData = new FormData();
|
| 577 |
+
formData.append('file', file);
|
| 578 |
+
|
| 579 |
+
showSection('loadingSection');
|
| 580 |
+
|
| 581 |
+
try {
|
| 582 |
+
const response = await fetch('/upload-csv', {
|
| 583 |
+
method: 'POST',
|
| 584 |
+
body: formData
|
| 585 |
+
});
|
| 586 |
+
|
| 587 |
+
const result = await response.json();
|
| 588 |
+
|
| 589 |
+
if (result.success) {
|
| 590 |
+
guests = result.guests;
|
| 591 |
+
selectedGuests = [];
|
| 592 |
+
displayGuests();
|
| 593 |
+
showSection('guestSection');
|
| 594 |
+
showMessage(result.message, 'success');
|
| 595 |
+
} else {
|
| 596 |
+
showSection('uploadSection');
|
| 597 |
+
showMessage(result.error, 'error');
|
| 598 |
+
}
|
| 599 |
+
} catch (error) {
|
| 600 |
+
showSection('uploadSection');
|
| 601 |
+
showMessage('Upload failed. Please try again.', 'error');
|
| 602 |
+
}
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
function displayGuests() {
|
| 606 |
+
const guestsGrid = document.getElementById('guestsGrid');
|
| 607 |
+
guestsGrid.innerHTML = '';
|
| 608 |
+
|
| 609 |
+
guests.forEach(guest => {
|
| 610 |
+
const guestCard = document.createElement('div');
|
| 611 |
+
guestCard.className = 'guest-card';
|
| 612 |
+
guestCard.onclick = () => toggleGuestSelection(guest);
|
| 613 |
+
|
| 614 |
+
guestCard.innerHTML = `
|
| 615 |
+
<input type="checkbox" class="guest-checkbox" ${selectedGuests.includes(guest.id) ? 'checked' : ''}>
|
| 616 |
+
<div class="guest-name">${guest.name}</div>
|
| 617 |
+
<div class="guest-title">${guest.title}</div>
|
| 618 |
+
`;
|
| 619 |
+
|
| 620 |
+
guestsGrid.appendChild(guestCard);
|
| 621 |
+
});
|
| 622 |
+
|
| 623 |
+
updateStats();
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
function toggleGuestSelection(guest) {
|
| 627 |
+
const index = selectedGuests.indexOf(guest.id);
|
| 628 |
+
if (index > -1) {
|
| 629 |
+
selectedGuests.splice(index, 1);
|
| 630 |
+
} else {
|
| 631 |
+
selectedGuests.push(guest.id);
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
displayGuests();
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
function updateStats() {
|
| 638 |
+
document.getElementById('totalGuests').textContent = guests.length;
|
| 639 |
+
document.getElementById('selectedGuests').textContent = selectedGuests.length;
|
| 640 |
+
|
| 641 |
+
// Calculate number of tables needed
|
| 642 |
+
const numTables = Math.ceil(selectedGuests.length / 10);
|
| 643 |
+
document.getElementById('remainingSlots').textContent = numTables;
|
| 644 |
+
|
| 645 |
+
document.getElementById('arrangeBtn').disabled = selectedGuests.length === 0;
|
| 646 |
+
}
|
| 647 |
+
|
| 648 |
+
document.getElementById('arrangeBtn').addEventListener('click', arrangeTables);
|
| 649 |
+
|
| 650 |
+
async function arrangeTables() {
|
| 651 |
+
const selectedGuestObjects = guests.filter(guest => selectedGuests.includes(guest.id));
|
| 652 |
+
|
| 653 |
+
try {
|
| 654 |
+
const response = await fetch('/arrange-tables', {
|
| 655 |
+
method: 'POST',
|
| 656 |
+
headers: {
|
| 657 |
+
'Content-Type': 'application/json'
|
| 658 |
+
},
|
| 659 |
+
body: JSON.stringify({
|
| 660 |
+
selected_guests: selectedGuestObjects
|
| 661 |
+
})
|
| 662 |
+
});
|
| 663 |
+
|
| 664 |
+
const result = await response.json();
|
| 665 |
+
|
| 666 |
+
if (result.success) {
|
| 667 |
+
displayTables(result.tables);
|
| 668 |
+
showSection('tablesSection');
|
| 669 |
+
} else {
|
| 670 |
+
showMessage(result.error, 'error');
|
| 671 |
+
}
|
| 672 |
+
} catch (error) {
|
| 673 |
+
showMessage('Failed to arrange tables. Please try again.', 'error');
|
| 674 |
+
}
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
function displayTables(tables) {
|
| 678 |
+
const tablesContainer = document.getElementById('tablesContainer');
|
| 679 |
+
tablesContainer.innerHTML = '';
|
| 680 |
+
|
| 681 |
+
tables.forEach((table, index) => {
|
| 682 |
+
const tableCard = document.createElement('div');
|
| 683 |
+
tableCard.className = 'table-card';
|
| 684 |
+
|
| 685 |
+
// Create circular table visualization
|
| 686 |
+
const circularTable = createCircularTable(table, index + 1);
|
| 687 |
+
|
| 688 |
+
tableCard.innerHTML = `
|
| 689 |
+
<div class="table-header">
|
| 690 |
+
<div class="table-title">Table ${index + 1}</div>
|
| 691 |
+
<div class="table-count">${table.length} guests</div>
|
| 692 |
+
</div>
|
| 693 |
+
${circularTable}
|
| 694 |
+
`;
|
| 695 |
+
|
| 696 |
+
tablesContainer.appendChild(tableCard);
|
| 697 |
+
});
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
function createCircularTable(guests, tableNumber) {
|
| 701 |
+
if (guests.length === 0) {
|
| 702 |
+
return '<div class="circular-table"><div class="table-center">Empty</div></div>';
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
const tableRadius = 120; // Radius of the table
|
| 706 |
+
const seatRadius = 25; // Radius of each seat
|
| 707 |
+
const centerX = 150;
|
| 708 |
+
const centerY = 150;
|
| 709 |
+
|
| 710 |
+
let tableHTML = `
|
| 711 |
+
<div class="circular-table">
|
| 712 |
+
<div class="table-center">Table ${tableNumber}</div>
|
| 713 |
+
`;
|
| 714 |
+
|
| 715 |
+
// Calculate positions for seats around the table
|
| 716 |
+
guests.forEach((guest, index) => {
|
| 717 |
+
const angle = (index / guests.length) * 2 * Math.PI - Math.PI / 2; // Start from top
|
| 718 |
+
const x = centerX + tableRadius * Math.cos(angle);
|
| 719 |
+
const y = centerY + tableRadius * Math.sin(angle);
|
| 720 |
+
|
| 721 |
+
// Get initials for display
|
| 722 |
+
const initials = guest.name.split(' ').map(n => n[0]).join('').toUpperCase();
|
| 723 |
+
|
| 724 |
+
tableHTML += `
|
| 725 |
+
<div class="seat" style="left: ${x - seatRadius}px; top: ${y - seatRadius}px;">
|
| 726 |
+
${initials}
|
| 727 |
+
<div class="seat-info">
|
| 728 |
+
<strong>${guest.name}</strong><br>
|
| 729 |
+
${guest.title}
|
| 730 |
+
</div>
|
| 731 |
+
</div>
|
| 732 |
+
`;
|
| 733 |
+
});
|
| 734 |
+
|
| 735 |
+
tableHTML += '</div>';
|
| 736 |
+
return tableHTML;
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
document.getElementById('backToGuestsBtn').addEventListener('click', () => {
|
| 740 |
+
showSection('guestSection');
|
| 741 |
+
});
|
| 742 |
+
|
| 743 |
+
function showSection(sectionId) {
|
| 744 |
+
document.querySelectorAll('.card').forEach(card => {
|
| 745 |
+
card.classList.add('hidden');
|
| 746 |
+
});
|
| 747 |
+
document.getElementById(sectionId).classList.remove('hidden');
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
function showMessage(message, type) {
|
| 751 |
+
const existingMessage = document.querySelector('.error-message, .success-message');
|
| 752 |
+
if (existingMessage) {
|
| 753 |
+
existingMessage.remove();
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
const messageDiv = document.createElement('div');
|
| 757 |
+
messageDiv.className = type === 'error' ? 'error-message' : 'success-message';
|
| 758 |
+
messageDiv.textContent = message;
|
| 759 |
+
|
| 760 |
+
const container = document.querySelector('.container');
|
| 761 |
+
container.insertBefore(messageDiv, container.firstChild);
|
| 762 |
+
|
| 763 |
+
setTimeout(() => {
|
| 764 |
+
messageDiv.remove();
|
| 765 |
+
}, 5000);
|
| 766 |
+
}
|
| 767 |
+
|
| 768 |
+
document.getElementById('selectAllBtn').addEventListener('click', () => {
|
| 769 |
+
selectedGuests = guests.map(guest => guest.id);
|
| 770 |
+
displayGuests();
|
| 771 |
+
});
|
| 772 |
+
|
| 773 |
+
document.getElementById('deselectAllBtn').addEventListener('click', () => {
|
| 774 |
+
selectedGuests = [];
|
| 775 |
+
displayGuests();
|
| 776 |
+
});
|
| 777 |
+
</script>
|
| 778 |
+
</body>
|
| 779 |
+
</html>
|