Spaces:
Build error
Build error
Upload 54 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- Readme.md +227 -0
- __pycache__/app.cpython-310.pyc +0 -0
- __pycache__/models.cpython-310.pyc +0 -0
- __pycache__/tools.cpython-310.pyc +0 -0
- ads/__init__.py +10 -0
- ads/__pycache__/__init__.cpython-310.pyc +0 -0
- ads/__pycache__/google_ads.cpython-310.pyc +0 -0
- ads/ads_config.json +28 -0
- ads/clicks.json +1 -0
- ads/google_ads.py +376 -0
- ads/impressions.json +1 -0
- app.py +1242 -0
- magicAi.db +0 -0
- models.py +21 -0
- prompts/__init__.py +13 -0
- prompts/__pycache__/__init__.cpython-310.pyc +0 -0
- prompts/__pycache__/prompt_marketplace.cpython-310.pyc +0 -0
- prompts/__pycache__/prompt_templates.cpython-310.pyc +0 -0
- prompts/marketplace_data/purchases.json +1 -0
- prompts/marketplace_data/sales.json +1 -0
- prompts/marketplace_data/stats.json +6 -0
- prompts/prompt_marketplace.py +728 -0
- prompts/prompt_templates.py +350 -0
- prompts/templates/code_assistant.json +37 -0
- prompts/templates/creative_writing.json +30 -0
- prompts/templates/general_chat.json +14 -0
- prompts/templates/image_prompt.json +39 -0
- providers/__init__.py +41 -0
- providers/__pycache__/__init__.cpython-310.pyc +0 -0
- providers/__pycache__/deepseek.cpython-310.pyc +0 -0
- providers/__pycache__/huggingface.cpython-310.pyc +0 -0
- providers/__pycache__/openai.cpython-310.pyc +0 -0
- providers/__pycache__/openrouter.cpython-310.pyc +0 -0
- providers/deepseek.py +210 -0
- providers/huggingface.py +193 -0
- providers/openai.py +178 -0
- providers/openrouter.py +164 -0
- requirements.txt +35 -0
- static/css/styles.css +737 -0
- static/js/main.js +260 -0
- templates/ad.html +160 -0
- templates/ad_reward.html +226 -0
- templates/admin_dashboard.html +238 -0
- templates/base.html +217 -0
- templates/credits.html +296 -0
- templates/error.html +87 -0
- templates/index.html +211 -0
- templates/login.html +104 -0
- templates/marketplace.html +287 -0
- templates/register.html +93 -0
Readme.md
ADDED
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
🚀 Comprehensive Strategy for Building an AI Tools Platform with Ad-Based Monetization (AWS Focused for 1 Lakh DAUs)
|
2 |
+
🔍 Vision
|
3 |
+
Build a low-cost yet scalable AI tools platform where users can access various AI services (text, image, audio, etc.) by watching ads. Each tool will have dynamic credit allocation — text tools (1 min ad), image tools (2 min ad), etc.
|
4 |
+
|
5 |
+
📐 Architecture Blueprint
|
6 |
+
A robust, scalable, and cost-effective architecture will ensure smooth performance for 1 lakh DAUs.
|
7 |
+
|
8 |
+
🧩 Key Components
|
9 |
+
Frontend: Html/css/js
|
10 |
+
Backend: FastAPI / Flask (for managing AI tool requests)
|
11 |
+
AI Models: Hugging Face, DeepSeek, OpenRouter, etc.
|
12 |
+
Database: DynamoDB / PostgreSQL (low latency, scalable)
|
13 |
+
Cache Layer: Redis / ElastiCache (to reduce API costs)
|
14 |
+
Ad System: Google AdSense, AdMob, or Revcontent
|
15 |
+
Deployment & Scaling: AWS ECS + Fargate (serverless scaling)
|
16 |
+
CDN for Speed: Cloudflare (faster static content delivery)
|
17 |
+
Authentication: AWS Cognito / Auth0 for secure logins
|
18 |
+
🏗️ System Design Flow
|
19 |
+
✅ Step 1: User visits the platform and selects an AI tool.
|
20 |
+
✅ Step 2: Platform verifies user's credit balance.
|
21 |
+
|
22 |
+
🔸 If sufficient credits → Access tool directly.
|
23 |
+
🔸 If insufficient credits → Show an ad to earn credits.
|
24 |
+
✅ Step 3: Credits are dynamically assigned based on the tool:
|
25 |
+
🔹 Text Models: 1 Min Ad → +5 Credits
|
26 |
+
🔹 Image Models: 2 Min Ad → +10 Credits
|
27 |
+
User custom Promts by user where user edit the make their own uses and user who created gets cut for promts 2% of model model tool creadit
|
28 |
+
✅ Step 4: User request is processed via FastAPI backend.
|
29 |
+
✅ Step 5: AI Model API is triggered (DeepSeek, Mistral, OpenRouter, etc.)
|
30 |
+
✅ Step 6: Result is stored in DynamoDB and cached via Redis for repeat queries.
|
31 |
+
|
32 |
+
|
33 |
+
|
34 |
+
Tool Type Ad Watch Time Credits Earned Estimated Cost Per Request
|
35 |
+
Text Models 1 Minute Ad +5 Credits ₹0.01 - ₹0.05 per request
|
36 |
+
Image Models 2 Minute Ad +10 Credits ₹0.10 - ₹0.50 per request
|
37 |
+
Video Models 3 Minute Ad +15 Credits ₹0.50 - ₹1.00 per request
|
38 |
+
|
39 |
+
|
40 |
+
|
41 |
+
⚙️ Technical Stack (Optimized for AWS and Cost Efficiency)
|
42 |
+
Component Recommended Solution
|
43 |
+
Frontend Streamlit + React (for hybrid UI needs)
|
44 |
+
Backend FastAPI (best for speed & scalability)
|
45 |
+
AI Model Hosting AWS Lambda (for lightweight AI models)
|
46 |
+
AI Model APIs Hugging Face / DeepSeek API
|
47 |
+
Database DynamoDB (serverless, scalable)
|
48 |
+
Cache Redis (ElastiCache for low latency)
|
49 |
+
Ad System Google AdSense / AdMob
|
50 |
+
Deployment AWS ECS (with Fargate for auto-scaling)
|
51 |
+
CDN Cloudflare (for global content delivery)
|
52 |
+
Auth AWS Cognito (scalable user management)
|
53 |
+
|
54 |
+
|
55 |
+
💰 Cost Optimization Plan for 1 Lakh DAUs
|
56 |
+
Component Estimated Cost (₹/month) Optimization Strategy
|
57 |
+
AWS ECS + Fargate ₹18,000 - ₹25,000 Efficient container scaling
|
58 |
+
DynamoDB (Database) ₹5,000 - ₹7,000 Use on-demand mode
|
59 |
+
Redis (ElastiCache) ₹3,000 - ₹5,000 Cache frequently accessed data
|
60 |
+
AI Model API Usage ₹20,000 - ₹40,000 Optimize prompt structure
|
61 |
+
Cloudflare (CDN) ₹5,000 - ₹8,000 Leverage caching for static files
|
62 |
+
Google AdSense Revenue ₹1,20,000 - ₹1,80,000 Based on ad engagement (30% conversion)
|
63 |
+
✅ Projected Net Profit Estimate: ₹60,000 - ₹1,00,000 (assuming 40% user engagement)
|
64 |
+
|
65 |
+
🧮 Credit System with Dynamic Scaling
|
66 |
+
Tool Type Ad Watch Time Credits Earned Estimated Cost Per Request
|
67 |
+
Text Models 1 Minute Ad +5 Credits ₹0.01 - ₹0.05 per request
|
68 |
+
Image Models 2 Minute Ad +10 Credits ₹0.10 - ₹0.50 per request
|
69 |
+
Video Models 3 Minute Ad +15 Credits ₹0.50 - ₹1.00 per request
|
70 |
+
|
71 |
+
✅ Logic: Higher resource-intensive models require longer ad watch times.
|
72 |
+
|
73 |
+
📋 Project Structure (Best Practices)
|
74 |
+
|
75 |
+
/app
|
76 |
+
├── /frontend
|
77 |
+
│ ├── main.py
|
78 |
+
│ ├── pages/
|
79 |
+
│ ├── components/
|
80 |
+
| UI/
|
81 |
+
├── /backend
|
82 |
+
│ ├── api.py
|
83 |
+
│ ├── credit_manager.py
|
84 |
+
│ ├── ad_manager.py
|
85 |
+
│ └── ai_service.py
|
86 |
+
├── /database
|
87 |
+
│ ├── db_connector.py
|
88 |
+
│ └── credit_tracker.py
|
89 |
+
├── /models
|
90 |
+
│ ├── text_gen_model.py
|
91 |
+
│ ├── image_gen_model.py
|
92 |
+
│ └── video_gen_model.py
|
93 |
+
├── Dockerfile
|
94 |
+
├── requirements.txt
|
95 |
+
├── .env
|
96 |
+
└── config.yaml
|
97 |
+
|
98 |
+
🔐 Security Best Practices
|
99 |
+
✅ AWS Cognito for user authentication.
|
100 |
+
✅ IAM Role Management to control resource access.
|
101 |
+
✅ Use CloudWatch for monitoring performance and security threats.
|
102 |
+
✅ Implement Rate Limiting for API abuse prevention.
|
103 |
+
✅ Set SSL/TLS encryption for secure data transmission.
|
104 |
+
|
105 |
+
📈 Scaling Strategy for 1 Lakh DAUs
|
106 |
+
✅ ECS Auto-Scaling Policies: Use CPU & Memory-based scaling triggers.
|
107 |
+
✅ DynamoDB Auto-Scaling: Set capacity limits with automatic scale-up.
|
108 |
+
✅ Implement Cloudflare CDN for fast content delivery.
|
109 |
+
✅ Optimize API requests using batch processing to minimize load.
|
110 |
+
✅ Use Lambda Edge for regional content caching.
|
111 |
+
|
112 |
+
🔊 Ad Revenue Optimization Strategy
|
113 |
+
✅ Use Google AdSense Video Ads for high-payout ads.
|
114 |
+
✅ Add Interactive Ads to boost engagement.
|
115 |
+
✅ Introduce Rewarded Ads (watch longer ads for bonus credits).
|
116 |
+
✅ Implement a Referral System to increase user retention.
|
117 |
+
|
118 |
+
✅ Step-by-Step Development Plan
|
119 |
+
1️⃣ Create Streamlit Frontend → Design dynamic UI with credit-based access.
|
120 |
+
2️⃣ Build Backend (FastAPI/Flask) → Integrate AI model APIs with token logic.
|
121 |
+
3️⃣ Set Up Ad Management System → Implement Google AdSense/AdMob integration.
|
122 |
+
4️⃣ Implement Credit-Based Workflow → Map credit logic to ad-watch duration.
|
123 |
+
5️⃣ Optimize AI Model Costs → Use caching (Redis) to reduce redundant calls.
|
124 |
+
6️⃣ Deploy on AWS ECS + Fargate → Set up auto-scaling for cost control.
|
125 |
+
7️⃣ Add Analytics → Track user behavior, ad conversion, and credit consumption.
|
126 |
+
|
127 |
+
🎯 Bonus Features for Maximum Engagement
|
128 |
+
✅ Leaderboard System: Users earn bonus credits by inviting friends.
|
129 |
+
✅ Daily Login Rewards: Encourage repeat visits with small bonuses.
|
130 |
+
✅ Premium Subscription Model: Offer ad-free premium access with special tools.
|
131 |
+
✅ Limited-Time Offers: Drive engagement with exclusive tool unlocks.
|
132 |
+
|
133 |
+
# MegicAI Platform
|
134 |
+
|
135 |
+
Multi-provider AI platform with credit system and ad-based monetization.
|
136 |
+
|
137 |
+
## Features
|
138 |
+
|
139 |
+
- **Multiple AI Providers**: Support for OpenAI, Hugging Face, and OpenRouter
|
140 |
+
- **Fallback Mechanism**: Automatically switches to available providers if one fails
|
141 |
+
- **Credit System**: Users earn credits by watching ads
|
142 |
+
- **Modern UI**: Professional interface with animations and responsive design
|
143 |
+
- **Tool Selection**: Various AI tools for different use cases (text, image, video, etc.)
|
144 |
+
- **Model Selection**: Choose specific AI provider for each request
|
145 |
+
|
146 |
+
## Quick Start
|
147 |
+
|
148 |
+
### Prerequisites
|
149 |
+
|
150 |
+
- Python 3.8+
|
151 |
+
- Redis server (for caching)
|
152 |
+
|
153 |
+
### Installation
|
154 |
+
|
155 |
+
1. Clone the repository:
|
156 |
+
```
|
157 |
+
git clone https://github.com/yourusername/megicai.git
|
158 |
+
cd megicai
|
159 |
+
```
|
160 |
+
|
161 |
+
2. Install dependencies:
|
162 |
+
```
|
163 |
+
pip install -r requirements.txt
|
164 |
+
```
|
165 |
+
|
166 |
+
3. Start the application (both backend and frontend):
|
167 |
+
```
|
168 |
+
python start.py
|
169 |
+
```
|
170 |
+
|
171 |
+
4. Access the application:
|
172 |
+
- Frontend: http://localhost:8501
|
173 |
+
- Backend API: http://localhost:8000
|
174 |
+
|
175 |
+
## Development Setup
|
176 |
+
|
177 |
+
1. Install development dependencies:
|
178 |
+
```
|
179 |
+
pip install -r requirements-dev.txt
|
180 |
+
```
|
181 |
+
|
182 |
+
2. Run backend server only:
|
183 |
+
```
|
184 |
+
python backend/run_server.py backend.api_minimal
|
185 |
+
```
|
186 |
+
|
187 |
+
3. Run frontend only:
|
188 |
+
```
|
189 |
+
streamlit run frontend/main.py
|
190 |
+
```
|
191 |
+
|
192 |
+
## Production Deployment
|
193 |
+
|
194 |
+
### Docker Deployment
|
195 |
+
|
196 |
+
1. Build the Docker image:
|
197 |
+
```
|
198 |
+
docker build -t megicai:latest .
|
199 |
+
```
|
200 |
+
|
201 |
+
2. Run with Docker Compose:
|
202 |
+
```
|
203 |
+
docker-compose up -d
|
204 |
+
```
|
205 |
+
|
206 |
+
### AWS Deployment
|
207 |
+
|
208 |
+
1. Set up the required AWS resources:
|
209 |
+
- ECS cluster for containerized deployment
|
210 |
+
- ElastiCache (Redis) for caching
|
211 |
+
- DynamoDB for user data and credits
|
212 |
+
- Cognito for authentication
|
213 |
+
|
214 |
+
2. Configure environment variables in AWS Parameter Store or Secrets Manager.
|
215 |
+
|
216 |
+
3. Deploy using the AWS CDK or CloudFormation template in the `deployment` directory.
|
217 |
+
|
218 |
+
## Configuration
|
219 |
+
|
220 |
+
Edit `config.yaml` to configure:
|
221 |
+
- AI provider API keys
|
222 |
+
- Redis connection details
|
223 |
+
- Credit system parameters
|
224 |
+
|
225 |
+
## License
|
226 |
+
|
227 |
+
MIT
|
__pycache__/app.cpython-310.pyc
ADDED
Binary file (26.4 kB). View file
|
|
__pycache__/models.cpython-310.pyc
ADDED
Binary file (943 Bytes). View file
|
|
__pycache__/tools.cpython-310.pyc
ADDED
Binary file (7.25 kB). View file
|
|
ads/__init__.py
ADDED
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Ads Package
|
3 |
+
Exports Google Ads integration classes
|
4 |
+
"""
|
5 |
+
|
6 |
+
from ads.google_ads import GoogleAdsManager
|
7 |
+
|
8 |
+
__all__ = [
|
9 |
+
'GoogleAdsManager'
|
10 |
+
]
|
ads/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (270 Bytes). View file
|
|
ads/__pycache__/google_ads.cpython-310.pyc
ADDED
Binary file (11.6 kB). View file
|
|
ads/ads_config.json
ADDED
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"publisher_id": "",
|
3 |
+
"ad_units": {
|
4 |
+
"sidebar": {
|
5 |
+
"ad_unit_id": "1234567890",
|
6 |
+
"ad_format": "display",
|
7 |
+
"width": 300,
|
8 |
+
"height": 250,
|
9 |
+
"slot": "sidebar-ad",
|
10 |
+
"enabled": true
|
11 |
+
},
|
12 |
+
"footer": {
|
13 |
+
"ad_unit_id": "0987654321",
|
14 |
+
"ad_format": "display",
|
15 |
+
"width": 728,
|
16 |
+
"height": 90,
|
17 |
+
"slot": "footer-ad",
|
18 |
+
"enabled": true
|
19 |
+
},
|
20 |
+
"reward_video": {
|
21 |
+
"ad_unit_id": "5678901234",
|
22 |
+
"ad_format": "video",
|
23 |
+
"slot": "reward-video-ad",
|
24 |
+
"enabled": true,
|
25 |
+
"reward_credits": 10
|
26 |
+
}
|
27 |
+
}
|
28 |
+
}
|
ads/clicks.json
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
[]
|
ads/google_ads.py
ADDED
@@ -0,0 +1,376 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Google Ads Integration
|
3 |
+
Handles displaying Google AdSense ads and tracking ad impressions/clicks
|
4 |
+
"""
|
5 |
+
import os
|
6 |
+
import uuid
|
7 |
+
import logging
|
8 |
+
import json
|
9 |
+
import time
|
10 |
+
from typing import Dict, Any, Optional, List
|
11 |
+
from datetime import datetime
|
12 |
+
|
13 |
+
# Setup logging
|
14 |
+
logging.basicConfig(level=logging.INFO)
|
15 |
+
logger = logging.getLogger("google_ads")
|
16 |
+
|
17 |
+
class GoogleAdsManager:
|
18 |
+
"""Manages Google AdSense integration and tracking"""
|
19 |
+
|
20 |
+
def __init__(self, publisher_id: Optional[str] = None, config_file: str = None):
|
21 |
+
"""Initialize Google Ads Manager"""
|
22 |
+
self.publisher_id = publisher_id or os.getenv("GOOGLE_ADSENSE_PUBLISHER_ID", "")
|
23 |
+
self.config_file = config_file or os.path.join(os.path.dirname(__file__), "ads_config.json")
|
24 |
+
|
25 |
+
# Load ad units configuration
|
26 |
+
self.ad_units = self._load_ad_units()
|
27 |
+
|
28 |
+
# Track impressions and clicks
|
29 |
+
self.impressions_file = os.path.join(os.path.dirname(__file__), "impressions.json")
|
30 |
+
self.clicks_file = os.path.join(os.path.dirname(__file__), "clicks.json")
|
31 |
+
|
32 |
+
# Create tracking files if they don't exist
|
33 |
+
self._initialize_tracking_files()
|
34 |
+
|
35 |
+
def _load_ad_units(self) -> Dict[str, Any]:
|
36 |
+
"""Load ad units configuration"""
|
37 |
+
default_ad_units = {
|
38 |
+
"sidebar": {
|
39 |
+
"ad_unit_id": "1234567890",
|
40 |
+
"ad_format": "display",
|
41 |
+
"width": 300,
|
42 |
+
"height": 250,
|
43 |
+
"slot": "sidebar-ad",
|
44 |
+
"enabled": True
|
45 |
+
},
|
46 |
+
"footer": {
|
47 |
+
"ad_unit_id": "0987654321",
|
48 |
+
"ad_format": "display",
|
49 |
+
"width": 728,
|
50 |
+
"height": 90,
|
51 |
+
"slot": "footer-ad",
|
52 |
+
"enabled": True
|
53 |
+
},
|
54 |
+
"reward_video": {
|
55 |
+
"ad_unit_id": "5678901234",
|
56 |
+
"ad_format": "video",
|
57 |
+
"slot": "reward-video-ad",
|
58 |
+
"enabled": True,
|
59 |
+
"reward_credits": 10
|
60 |
+
}
|
61 |
+
}
|
62 |
+
|
63 |
+
# Create config file with default values if it doesn't exist
|
64 |
+
if not os.path.exists(self.config_file):
|
65 |
+
os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
|
66 |
+
with open(self.config_file, "w") as f:
|
67 |
+
json.dump({"publisher_id": self.publisher_id, "ad_units": default_ad_units}, f, indent=2)
|
68 |
+
return default_ad_units
|
69 |
+
|
70 |
+
# Load config from file
|
71 |
+
try:
|
72 |
+
with open(self.config_file, "r") as f:
|
73 |
+
config = json.load(f)
|
74 |
+
|
75 |
+
# Update publisher ID if it was set in the config file
|
76 |
+
if config.get("publisher_id"):
|
77 |
+
self.publisher_id = config["publisher_id"]
|
78 |
+
|
79 |
+
return config.get("ad_units", default_ad_units)
|
80 |
+
except Exception as e:
|
81 |
+
logger.error(f"Error loading ad units config: {e}")
|
82 |
+
return default_ad_units
|
83 |
+
|
84 |
+
def _initialize_tracking_files(self):
|
85 |
+
"""Initialize tracking files if they don't exist"""
|
86 |
+
os.makedirs(os.path.dirname(self.impressions_file), exist_ok=True)
|
87 |
+
|
88 |
+
if not os.path.exists(self.impressions_file):
|
89 |
+
with open(self.impressions_file, "w") as f:
|
90 |
+
json.dump([], f)
|
91 |
+
|
92 |
+
if not os.path.exists(self.clicks_file):
|
93 |
+
with open(self.clicks_file, "w") as f:
|
94 |
+
json.dump([], f)
|
95 |
+
|
96 |
+
def get_ad_code(self, ad_position: str) -> Dict[str, Any]:
|
97 |
+
"""
|
98 |
+
Get HTML/JS code for displaying an ad at the specified position
|
99 |
+
Returns both the ad code and metadata about the ad
|
100 |
+
"""
|
101 |
+
if not self.publisher_id:
|
102 |
+
logger.warning("No Google AdSense publisher ID configured")
|
103 |
+
return {"success": False, "error": "No publisher ID configured"}
|
104 |
+
|
105 |
+
# Get ad unit configuration
|
106 |
+
ad_unit = self.ad_units.get(ad_position)
|
107 |
+
if not ad_unit:
|
108 |
+
logger.error(f"Ad position '{ad_position}' not configured")
|
109 |
+
return {"success": False, "error": f"Ad position '{ad_position}' not found"}
|
110 |
+
|
111 |
+
if not ad_unit.get("enabled", True):
|
112 |
+
logger.info(f"Ad unit '{ad_position}' is disabled")
|
113 |
+
return {"success": False, "error": "Ad unit is disabled"}
|
114 |
+
|
115 |
+
# Generate HTML/JS code for the ad
|
116 |
+
ad_format = ad_unit.get("ad_format", "display")
|
117 |
+
ad_unit_id = ad_unit.get("ad_unit_id", "")
|
118 |
+
ad_slot = ad_unit.get("slot", f"{ad_position}-ad")
|
119 |
+
|
120 |
+
if ad_format == "display":
|
121 |
+
width = ad_unit.get("width", 300)
|
122 |
+
height = ad_unit.get("height", 250)
|
123 |
+
|
124 |
+
ad_code = f"""
|
125 |
+
<ins class="adsbygoogle"
|
126 |
+
style="display:inline-block;width:{width}px;height:{height}px"
|
127 |
+
data-ad-client="ca-pub-{self.publisher_id}"
|
128 |
+
data-ad-slot="{ad_unit_id}"></ins>
|
129 |
+
<script>
|
130 |
+
(adsbygoogle = window.adsbygoogle || []).push({{}});
|
131 |
+
</script>
|
132 |
+
"""
|
133 |
+
elif ad_format == "video":
|
134 |
+
ad_code = f"""
|
135 |
+
<div id="{ad_slot}" class="reward-ad-container">
|
136 |
+
<div class="reward-ad-placeholder">
|
137 |
+
<p>Watch a video to earn {ad_unit.get('reward_credits', 5)} credits</p>
|
138 |
+
<button class="watch-ad-btn" onclick="loadRewardAd('{ad_slot}', '{ad_unit_id}', {ad_unit.get('reward_credits', 5)})">Watch Now</button>
|
139 |
+
</div>
|
140 |
+
</div>
|
141 |
+
"""
|
142 |
+
else:
|
143 |
+
logger.error(f"Unsupported ad format: {ad_format}")
|
144 |
+
return {"success": False, "error": f"Unsupported ad format: {ad_format}"}
|
145 |
+
|
146 |
+
# Generate a unique ID for tracking this ad impression
|
147 |
+
impression_id = str(uuid.uuid4())
|
148 |
+
|
149 |
+
# Record the impression for tracking
|
150 |
+
self._record_impression(impression_id, ad_position, ad_unit_id)
|
151 |
+
|
152 |
+
return {
|
153 |
+
"success": True,
|
154 |
+
"ad_code": ad_code,
|
155 |
+
"impression_id": impression_id,
|
156 |
+
"ad_position": ad_position,
|
157 |
+
"ad_format": ad_format,
|
158 |
+
"reward_credits": ad_unit.get("reward_credits", 0) if ad_format == "video" else 0
|
159 |
+
}
|
160 |
+
|
161 |
+
def _record_impression(self, impression_id: str, ad_position: str, ad_unit_id: str):
|
162 |
+
"""Record an ad impression for tracking"""
|
163 |
+
try:
|
164 |
+
# Load existing impressions
|
165 |
+
with open(self.impressions_file, "r") as f:
|
166 |
+
impressions = json.load(f)
|
167 |
+
|
168 |
+
# Add new impression
|
169 |
+
impressions.append({
|
170 |
+
"id": impression_id,
|
171 |
+
"timestamp": datetime.now().isoformat(),
|
172 |
+
"ad_position": ad_position,
|
173 |
+
"ad_unit_id": ad_unit_id
|
174 |
+
})
|
175 |
+
|
176 |
+
# Save updated impressions
|
177 |
+
with open(self.impressions_file, "w") as f:
|
178 |
+
json.dump(impressions, f, indent=2)
|
179 |
+
|
180 |
+
except Exception as e:
|
181 |
+
logger.error(f"Error recording ad impression: {e}")
|
182 |
+
|
183 |
+
def record_ad_click(self, impression_id: str, user_id: Optional[str] = None) -> bool:
|
184 |
+
"""Record an ad click for tracking"""
|
185 |
+
try:
|
186 |
+
# Load existing clicks
|
187 |
+
with open(self.clicks_file, "r") as f:
|
188 |
+
clicks = json.load(f)
|
189 |
+
|
190 |
+
# Add new click
|
191 |
+
clicks.append({
|
192 |
+
"impression_id": impression_id,
|
193 |
+
"user_id": user_id,
|
194 |
+
"timestamp": datetime.now().isoformat()
|
195 |
+
})
|
196 |
+
|
197 |
+
# Save updated clicks
|
198 |
+
with open(self.clicks_file, "w") as f:
|
199 |
+
json.dump(clicks, f, indent=2)
|
200 |
+
|
201 |
+
return True
|
202 |
+
|
203 |
+
except Exception as e:
|
204 |
+
logger.error(f"Error recording ad click: {e}")
|
205 |
+
return False
|
206 |
+
|
207 |
+
def record_reward_ad_completion(self, impression_id: str, user_id: str) -> Dict[str, Any]:
|
208 |
+
"""
|
209 |
+
Record completion of a reward ad and return reward information
|
210 |
+
|
211 |
+
Args:
|
212 |
+
impression_id: The unique ID of the ad impression
|
213 |
+
user_id: The user ID to reward
|
214 |
+
|
215 |
+
Returns:
|
216 |
+
Dictionary with reward information and success status
|
217 |
+
"""
|
218 |
+
try:
|
219 |
+
# Find the impression to determine the ad unit
|
220 |
+
with open(self.impressions_file, "r") as f:
|
221 |
+
impressions = json.load(f)
|
222 |
+
|
223 |
+
# Find the impression with matching ID
|
224 |
+
impression = next((imp for imp in impressions if imp.get("id") == impression_id), None)
|
225 |
+
if not impression:
|
226 |
+
logger.error(f"Impression ID {impression_id} not found")
|
227 |
+
return {
|
228 |
+
"success": False,
|
229 |
+
"error": "Invalid impression ID"
|
230 |
+
}
|
231 |
+
|
232 |
+
# Get the ad position from the impression
|
233 |
+
ad_position = impression.get("ad_position")
|
234 |
+
ad_unit = self.ad_units.get(ad_position)
|
235 |
+
|
236 |
+
if not ad_unit:
|
237 |
+
logger.error(f"Ad unit for position {ad_position} not found")
|
238 |
+
return {
|
239 |
+
"success": False,
|
240 |
+
"error": "Ad unit not found"
|
241 |
+
}
|
242 |
+
|
243 |
+
# Get the reward amount
|
244 |
+
reward_credits = ad_unit.get("reward_credits", 0)
|
245 |
+
|
246 |
+
# Record the ad completion (we could store this in a separate file)
|
247 |
+
self.record_ad_click(impression_id, user_id)
|
248 |
+
|
249 |
+
return {
|
250 |
+
"success": True,
|
251 |
+
"reward_credits": reward_credits,
|
252 |
+
"user_id": user_id,
|
253 |
+
"impression_id": impression_id,
|
254 |
+
"timestamp": datetime.now().isoformat()
|
255 |
+
}
|
256 |
+
|
257 |
+
except Exception as e:
|
258 |
+
logger.error(f"Error processing reward ad completion: {e}")
|
259 |
+
return {
|
260 |
+
"success": False,
|
261 |
+
"error": str(e)
|
262 |
+
}
|
263 |
+
|
264 |
+
def get_html_header_code(self) -> str:
|
265 |
+
"""Get the HTML code to include in the page header for AdSense"""
|
266 |
+
if not self.publisher_id:
|
267 |
+
return ""
|
268 |
+
|
269 |
+
return f"""
|
270 |
+
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-{self.publisher_id}"
|
271 |
+
crossorigin="anonymous"></script>
|
272 |
+
"""
|
273 |
+
|
274 |
+
def get_reward_ad_js(self) -> str:
|
275 |
+
"""Get the JavaScript code for handling reward ads"""
|
276 |
+
return """
|
277 |
+
<script>
|
278 |
+
let rewardAdLoaded = false;
|
279 |
+
let adEventListenersAdded = false;
|
280 |
+
let currentAdSlot = null;
|
281 |
+
let rewardAmount = 0;
|
282 |
+
|
283 |
+
function loadRewardAd(adSlot, adUnitId, credits) {
|
284 |
+
currentAdSlot = adSlot;
|
285 |
+
rewardAmount = credits;
|
286 |
+
|
287 |
+
// Show loading indicator
|
288 |
+
document.getElementById(adSlot).innerHTML = '<div class="loading-ad">Loading ad, please wait...</div>';
|
289 |
+
|
290 |
+
// Create a new ad container
|
291 |
+
const adContainer = document.createElement('div');
|
292 |
+
adContainer.id = adSlot + '-container';
|
293 |
+
document.getElementById(adSlot).appendChild(adContainer);
|
294 |
+
|
295 |
+
// Load the Google ad
|
296 |
+
const adManager = new google.ads.AdManager(adContainer);
|
297 |
+
|
298 |
+
// Set up the ad
|
299 |
+
adManager.setAdUnitPath(`/ca-pub-${publisherId}/${adUnitId}`);
|
300 |
+
adManager.setAdSize([300, 250]);
|
301 |
+
|
302 |
+
// Add event listeners
|
303 |
+
if (!adEventListenersAdded) {
|
304 |
+
adManager.addEventListener('loaded', onAdLoaded);
|
305 |
+
adManager.addEventListener('error', onAdError);
|
306 |
+
adManager.addEventListener('completed', onAdCompleted);
|
307 |
+
adEventListenersAdded = true;
|
308 |
+
}
|
309 |
+
|
310 |
+
// Load the ad
|
311 |
+
adManager.load();
|
312 |
+
}
|
313 |
+
|
314 |
+
function onAdLoaded() {
|
315 |
+
rewardAdLoaded = true;
|
316 |
+
document.getElementById(currentAdSlot).querySelector('.loading-ad').style.display = 'none';
|
317 |
+
}
|
318 |
+
|
319 |
+
function onAdError(error) {
|
320 |
+
document.getElementById(currentAdSlot).innerHTML =
|
321 |
+
`<div class="ad-error">Error loading ad: ${error.message}. Please try again later.</div>`;
|
322 |
+
}
|
323 |
+
|
324 |
+
function onAdCompleted() {
|
325 |
+
// Send a request to the server to record the completion and reward the user
|
326 |
+
fetch('/api/ads/reward', {
|
327 |
+
method: 'POST',
|
328 |
+
headers: {
|
329 |
+
'Content-Type': 'application/json'
|
330 |
+
},
|
331 |
+
body: JSON.stringify({
|
332 |
+
impression_id: currentImpressionId,
|
333 |
+
completed: true
|
334 |
+
})
|
335 |
+
})
|
336 |
+
.then(response => response.json())
|
337 |
+
.then(data => {
|
338 |
+
if (data.success) {
|
339 |
+
// Show success message
|
340 |
+
document.getElementById(currentAdSlot).innerHTML =
|
341 |
+
`<div class="reward-success">Congratulations! You earned ${rewardAmount} credits.</div>`;
|
342 |
+
|
343 |
+
// Update user credits display if available
|
344 |
+
const creditsDisplay = document.getElementById('user-credits');
|
345 |
+
if (creditsDisplay) {
|
346 |
+
const currentCredits = parseInt(creditsDisplay.innerText, 10);
|
347 |
+
creditsDisplay.innerText = currentCredits + rewardAmount;
|
348 |
+
}
|
349 |
+
} else {
|
350 |
+
document.getElementById(currentAdSlot).innerHTML =
|
351 |
+
`<div class="reward-error">Error: ${data.error}</div>`;
|
352 |
+
}
|
353 |
+
})
|
354 |
+
.catch(error => {
|
355 |
+
document.getElementById(currentAdSlot).innerHTML =
|
356 |
+
`<div class="reward-error">Error: Unable to process reward. Please try again.</div>`;
|
357 |
+
});
|
358 |
+
}
|
359 |
+
</script>
|
360 |
+
"""
|
361 |
+
|
362 |
+
# Example usage
|
363 |
+
if __name__ == "__main__":
|
364 |
+
# Initialize the ads manager
|
365 |
+
ads_manager = GoogleAdsManager()
|
366 |
+
|
367 |
+
# Get ad code for the sidebar
|
368 |
+
sidebar_ad = ads_manager.get_ad_code("sidebar")
|
369 |
+
print(f"Sidebar ad success: {sidebar_ad['success']}")
|
370 |
+
|
371 |
+
if sidebar_ad['success']:
|
372 |
+
print(f"Impression ID: {sidebar_ad['impression_id']}")
|
373 |
+
|
374 |
+
# Get the header code for AdSense
|
375 |
+
header_code = ads_manager.get_html_header_code()
|
376 |
+
print(f"Header code length: {len(header_code)}")
|
ads/impressions.json
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
[]
|
app.py
ADDED
@@ -0,0 +1,1242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
AI Tool Hub - Main Application
|
3 |
+
Integrates all components into a single FastAPI application
|
4 |
+
"""
|
5 |
+
import os
|
6 |
+
import json
|
7 |
+
import uuid
|
8 |
+
import secrets
|
9 |
+
import logging
|
10 |
+
from datetime import datetime, timedelta
|
11 |
+
from typing import Dict, Any, List, Optional, Union
|
12 |
+
|
13 |
+
# Load environment variables from .env file
|
14 |
+
from dotenv import load_dotenv
|
15 |
+
load_dotenv()
|
16 |
+
|
17 |
+
from fastapi import FastAPI, Request, Response, Depends, HTTPException, Form, Cookie, status
|
18 |
+
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
19 |
+
from fastapi.staticfiles import StaticFiles
|
20 |
+
from fastapi.templating import Jinja2Templates
|
21 |
+
from fastapi.security import APIKeyHeader, HTTPBearer, HTTPAuthorizationCredentials
|
22 |
+
from fastapi.middleware.cors import CORSMiddleware
|
23 |
+
from pydantic import BaseModel, Field
|
24 |
+
from sqlalchemy import create_engine
|
25 |
+
from sqlalchemy.orm import sessionmaker
|
26 |
+
|
27 |
+
# Import provider modules
|
28 |
+
from providers import get_provider, HuggingFaceProvider, OpenAIProvider, DeepSeekProvider, OpenRouterProvider
|
29 |
+
|
30 |
+
# Import prompt modules
|
31 |
+
from prompts import PromptTemplate, PromptTemplateManager, PromptMarketplace
|
32 |
+
|
33 |
+
# Import ads module
|
34 |
+
from ads import GoogleAdsManager
|
35 |
+
|
36 |
+
# Import additional modules
|
37 |
+
import random
|
38 |
+
import time
|
39 |
+
import hashlib
|
40 |
+
|
41 |
+
# Import the Base from models.py
|
42 |
+
from models import Base
|
43 |
+
|
44 |
+
# Import the TOOLS from tools.py
|
45 |
+
from tools import TOOLS
|
46 |
+
|
47 |
+
# Setup logging
|
48 |
+
logging.basicConfig(level=logging.INFO)
|
49 |
+
logger = logging.getLogger("app")
|
50 |
+
|
51 |
+
# Database setup
|
52 |
+
DATABASE_URL = "sqlite:///./magicAi.db" # Path to your SQLite database
|
53 |
+
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
54 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
55 |
+
|
56 |
+
# Create the database tables
|
57 |
+
Base.metadata.create_all(bind=engine)
|
58 |
+
|
59 |
+
# Create the FastAPI app
|
60 |
+
app = FastAPI(
|
61 |
+
title="AI Tool Hub",
|
62 |
+
description="A platform for using AI models with various tools and prompt templates",
|
63 |
+
version="1.0.0"
|
64 |
+
)
|
65 |
+
|
66 |
+
# Add CORS middleware
|
67 |
+
app.add_middleware(
|
68 |
+
CORSMiddleware,
|
69 |
+
allow_origins=["*"],
|
70 |
+
allow_credentials=True,
|
71 |
+
allow_methods=["*"],
|
72 |
+
allow_headers=["*"],
|
73 |
+
)
|
74 |
+
|
75 |
+
# Mount static files
|
76 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
77 |
+
|
78 |
+
# Set up Jinja2 templates
|
79 |
+
templates = Jinja2Templates(directory="templates")
|
80 |
+
|
81 |
+
# Initialize components
|
82 |
+
template_manager = PromptTemplateManager()
|
83 |
+
marketplace = PromptMarketplace()
|
84 |
+
ads_manager = GoogleAdsManager()
|
85 |
+
|
86 |
+
# Security
|
87 |
+
API_KEY_NAME = "X-API-Key"
|
88 |
+
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
|
89 |
+
security = HTTPBearer(auto_error=False)
|
90 |
+
|
91 |
+
# In-memory store of API keys for demo purpose
|
92 |
+
# In production, these would be stored in a database
|
93 |
+
API_KEYS = {}
|
94 |
+
|
95 |
+
# In-memory session store for demo purpose
|
96 |
+
# In production, these would be stored in a database or Redis
|
97 |
+
SESSIONS = {}
|
98 |
+
|
99 |
+
# Add new models for ad rewards
|
100 |
+
class AdRewardRequest(BaseModel):
|
101 |
+
ad_type: str # 'daily' or 'special'
|
102 |
+
impression_id: Optional[str] = None
|
103 |
+
|
104 |
+
class DailyReward(BaseModel):
|
105 |
+
user_id: str
|
106 |
+
date: str
|
107 |
+
count: int = 0
|
108 |
+
last_reward: Optional[str] = None
|
109 |
+
|
110 |
+
class SpecialReward(BaseModel):
|
111 |
+
user_id: str
|
112 |
+
date: str
|
113 |
+
claimed: bool = False
|
114 |
+
claimed_at: Optional[str] = None
|
115 |
+
|
116 |
+
# In-memory stores for rewards (in production, use a database)
|
117 |
+
DAILY_REWARDS = {}
|
118 |
+
SPECIAL_REWARDS = {}
|
119 |
+
|
120 |
+
# Models
|
121 |
+
class CreditUpdateRequest(BaseModel):
|
122 |
+
user_id: str
|
123 |
+
amount: float
|
124 |
+
reason: str = "manual"
|
125 |
+
|
126 |
+
class GenerateRequest(BaseModel):
|
127 |
+
prompt: str
|
128 |
+
model: str
|
129 |
+
provider: str
|
130 |
+
system_message: Optional[str] = None
|
131 |
+
max_tokens: Optional[int] = 1000
|
132 |
+
temperature: Optional[float] = 0.7
|
133 |
+
prompt_template_id: Optional[str] = None
|
134 |
+
template_variables: Optional[Dict[str, Any]] = None
|
135 |
+
|
136 |
+
class ImageGenerateRequest(BaseModel):
|
137 |
+
prompt: str
|
138 |
+
provider: str = "openai"
|
139 |
+
model: Optional[str] = None
|
140 |
+
size: Optional[str] = "1024x1024"
|
141 |
+
prompt_template_id: Optional[str] = None
|
142 |
+
template_variables: Optional[Dict[str, Any]] = None
|
143 |
+
|
144 |
+
class UserInfo(BaseModel):
|
145 |
+
id: str
|
146 |
+
username: str
|
147 |
+
email: str
|
148 |
+
credits: float = 10.0 # New users start with some free credits
|
149 |
+
created_at: str = Field(default_factory=lambda: datetime.now().isoformat())
|
150 |
+
last_login: str = Field(default_factory=lambda: datetime.now().isoformat())
|
151 |
+
role: str = "user"
|
152 |
+
|
153 |
+
# Models for prompt system
|
154 |
+
class Prompt(BaseModel):
|
155 |
+
id: str
|
156 |
+
tool_id: str
|
157 |
+
title: str
|
158 |
+
content: str
|
159 |
+
description: Optional[str] = None
|
160 |
+
creator_id: Optional[str] = None
|
161 |
+
is_default: bool = False
|
162 |
+
usage_count: int = 0
|
163 |
+
avg_rating: float = 0.0
|
164 |
+
created_at: str = None
|
165 |
+
updated_at: str = None
|
166 |
+
tags: List[str] = []
|
167 |
+
|
168 |
+
class PromptRating(BaseModel):
|
169 |
+
prompt_id: str
|
170 |
+
user_id: str
|
171 |
+
rating: int # 1-5
|
172 |
+
created_at: str
|
173 |
+
|
174 |
+
class UserPromptHistory(BaseModel):
|
175 |
+
user_id: str
|
176 |
+
prompt_id: str
|
177 |
+
tool_id: str
|
178 |
+
used_at: str
|
179 |
+
was_modified: bool = False
|
180 |
+
modifications: Optional[str] = None
|
181 |
+
|
182 |
+
# Mock database (replace with actual DynamoDB integration)
|
183 |
+
PROMPTS = {}
|
184 |
+
PROMPT_RATINGS = []
|
185 |
+
USER_PROMPT_HISTORY = []
|
186 |
+
|
187 |
+
# Initialize default prompts
|
188 |
+
def init_default_prompts():
|
189 |
+
default_prompts = [
|
190 |
+
{
|
191 |
+
"id": "text-summary-default",
|
192 |
+
"tool_id": "text-summary",
|
193 |
+
"title": "Concise Text Summary",
|
194 |
+
"content": "Summarize the following text in 3-5 bullet points highlighting the main ideas: {{input}}",
|
195 |
+
"description": "Creates a bulleted summary of any text",
|
196 |
+
"is_default": True,
|
197 |
+
"tags": ["summary", "concise", "bullets"]
|
198 |
+
},
|
199 |
+
{
|
200 |
+
"id": "image-portrait-default",
|
201 |
+
"tool_id": "image-generator",
|
202 |
+
"title": "Professional Portrait",
|
203 |
+
"content": "Create a professional portrait photo of {{subject}}, high quality, studio lighting, detailed features, professional attire",
|
204 |
+
"description": "Generates professional-looking portrait images",
|
205 |
+
"is_default": True,
|
206 |
+
"tags": ["portrait", "professional", "photo"]
|
207 |
+
},
|
208 |
+
{
|
209 |
+
"id": "text-blog-default",
|
210 |
+
"tool_id": "text-generator",
|
211 |
+
"title": "Blog Post Generator",
|
212 |
+
"content": "Write a comprehensive blog post about {{topic}}. Include an introduction, 3-5 main sections with subheadings, and a conclusion. Use a conversational tone and include practical examples where appropriate.",
|
213 |
+
"description": "Creates complete blog posts with proper structure",
|
214 |
+
"is_default": True,
|
215 |
+
"tags": ["blog", "writing", "content"]
|
216 |
+
},
|
217 |
+
{
|
218 |
+
"id": "code-python-default",
|
219 |
+
"tool_id": "code-generator",
|
220 |
+
"title": "Python Function",
|
221 |
+
"content": "Write a Python function that {{task}}. Include docstrings, type hints, error handling, and comments explaining your logic. Provide a small example of how to use the function.",
|
222 |
+
"description": "Generates well-structured Python functions",
|
223 |
+
"is_default": True,
|
224 |
+
"tags": ["python", "function", "code"]
|
225 |
+
}
|
226 |
+
]
|
227 |
+
|
228 |
+
for prompt in default_prompts:
|
229 |
+
prompt_obj = Prompt(
|
230 |
+
id=prompt["id"],
|
231 |
+
tool_id=prompt["tool_id"],
|
232 |
+
title=prompt["title"],
|
233 |
+
content=prompt["content"],
|
234 |
+
description=prompt["description"],
|
235 |
+
is_default=prompt["is_default"],
|
236 |
+
tags=prompt["tags"],
|
237 |
+
created_at=datetime.now().isoformat(),
|
238 |
+
updated_at=datetime.now().isoformat()
|
239 |
+
)
|
240 |
+
PROMPTS[prompt["id"]] = prompt_obj
|
241 |
+
|
242 |
+
# Call initialization at startup
|
243 |
+
init_default_prompts()
|
244 |
+
|
245 |
+
# Helper functions for prompt system
|
246 |
+
def get_prompts_for_tool(tool_id: str) -> List[Prompt]:
|
247 |
+
"""Get all prompts for a specific tool"""
|
248 |
+
return [p for p in PROMPTS.values() if p.tool_id == tool_id]
|
249 |
+
|
250 |
+
def get_default_prompts_for_tool(tool_id: str) -> List[Prompt]:
|
251 |
+
"""Get default prompts for a specific tool"""
|
252 |
+
return [p for p in PROMPTS.values() if p.tool_id == tool_id and p.is_default]
|
253 |
+
|
254 |
+
def get_trending_prompts(limit: int = 5) -> List[Prompt]:
|
255 |
+
"""Get trending prompts based on usage count and ratings"""
|
256 |
+
sorted_prompts = sorted(
|
257 |
+
PROMPTS.values(),
|
258 |
+
key=lambda p: (p.usage_count * 0.7 + p.avg_rating * 0.3),
|
259 |
+
reverse=True
|
260 |
+
)
|
261 |
+
return sorted_prompts[:limit]
|
262 |
+
|
263 |
+
def get_personalized_prompts(user_id: str, tool_id: str, limit: int = 3) -> List[Prompt]:
|
264 |
+
"""Get personalized prompt recommendations for a user and tool"""
|
265 |
+
# Get user history for this tool
|
266 |
+
user_tool_history = [h for h in USER_PROMPT_HISTORY
|
267 |
+
if h.user_id == user_id and h.tool_id == tool_id]
|
268 |
+
|
269 |
+
if not user_tool_history:
|
270 |
+
# If no history, return default prompts
|
271 |
+
return get_default_prompts_for_tool(tool_id)
|
272 |
+
|
273 |
+
# Count prompt usage
|
274 |
+
prompt_usage = {}
|
275 |
+
for history in user_tool_history:
|
276 |
+
prompt_usage[history.prompt_id] = prompt_usage.get(history.prompt_id, 0) + 1
|
277 |
+
|
278 |
+
# Get most used prompts
|
279 |
+
most_used_prompt_ids = sorted(prompt_usage.items(), key=lambda x: x[1], reverse=True)
|
280 |
+
most_used_prompts = [PROMPTS.get(pid) for pid, _ in most_used_prompt_ids[:limit]]
|
281 |
+
|
282 |
+
# If we don't have enough, add some default or trending ones
|
283 |
+
if len(most_used_prompts) < limit:
|
284 |
+
defaults = get_default_prompts_for_tool(tool_id)
|
285 |
+
for default in defaults:
|
286 |
+
if default not in most_used_prompts and len(most_used_prompts) < limit:
|
287 |
+
most_used_prompts.append(default)
|
288 |
+
|
289 |
+
return most_used_prompts
|
290 |
+
|
291 |
+
def record_prompt_usage(user_id: str, prompt_id: str, tool_id: str, was_modified: bool = False, modifications: str = None):
|
292 |
+
"""Record that a user used a particular prompt"""
|
293 |
+
# Update prompt usage count
|
294 |
+
if prompt_id in PROMPTS:
|
295 |
+
PROMPTS[prompt_id].usage_count += 1
|
296 |
+
PROMPTS[prompt_id].updated_at = datetime.now().isoformat()
|
297 |
+
|
298 |
+
# Add to history
|
299 |
+
history_entry = UserPromptHistory(
|
300 |
+
user_id=user_id,
|
301 |
+
prompt_id=prompt_id,
|
302 |
+
tool_id=tool_id,
|
303 |
+
used_at=datetime.now().isoformat(),
|
304 |
+
was_modified=was_modified,
|
305 |
+
modifications=modifications
|
306 |
+
)
|
307 |
+
USER_PROMPT_HISTORY.append(history_entry)
|
308 |
+
|
309 |
+
def rate_prompt(user_id: str, prompt_id: str, rating: int):
|
310 |
+
"""Add or update a rating for a prompt"""
|
311 |
+
# Check if user already rated this prompt
|
312 |
+
existing_rating = next((r for r in PROMPT_RATINGS if r.user_id == user_id and r.prompt_id == prompt_id), None)
|
313 |
+
|
314 |
+
if existing_rating:
|
315 |
+
# Update existing rating
|
316 |
+
existing_rating.rating = rating
|
317 |
+
existing_rating.created_at = datetime.now().isoformat()
|
318 |
+
else:
|
319 |
+
# Add new rating
|
320 |
+
rating_obj = PromptRating(
|
321 |
+
prompt_id=prompt_id,
|
322 |
+
user_id=user_id,
|
323 |
+
rating=rating,
|
324 |
+
created_at=datetime.now().isoformat()
|
325 |
+
)
|
326 |
+
PROMPT_RATINGS.append(rating_obj)
|
327 |
+
|
328 |
+
# Update average rating on prompt
|
329 |
+
if prompt_id in PROMPTS:
|
330 |
+
prompt_ratings = [r.rating for r in PROMPT_RATINGS if r.prompt_id == prompt_id]
|
331 |
+
if prompt_ratings:
|
332 |
+
PROMPTS[prompt_id].avg_rating = sum(prompt_ratings) / len(prompt_ratings)
|
333 |
+
PROMPTS[prompt_id].updated_at = datetime.now().isoformat()
|
334 |
+
|
335 |
+
# Dependency to get the database session
|
336 |
+
def get_db():
|
337 |
+
db = SessionLocal()
|
338 |
+
try:
|
339 |
+
yield db
|
340 |
+
finally:
|
341 |
+
db.close()
|
342 |
+
|
343 |
+
# User authentication and session management
|
344 |
+
def get_session_user(session_id: Optional[str] = Cookie(None)) -> Optional[UserInfo]:
|
345 |
+
"""Get the user from the session"""
|
346 |
+
if not session_id or session_id not in SESSIONS:
|
347 |
+
return None
|
348 |
+
|
349 |
+
session = SESSIONS[session_id]
|
350 |
+
if datetime.now() > session["expires"]:
|
351 |
+
# Session expired
|
352 |
+
del SESSIONS[session_id]
|
353 |
+
return None
|
354 |
+
|
355 |
+
# Update session expiration
|
356 |
+
session["expires"] = datetime.now() + timedelta(hours=24)
|
357 |
+
|
358 |
+
return session["user"]
|
359 |
+
|
360 |
+
def require_session_user(admin_required: bool = False, session_user: Optional[UserInfo] = Depends(get_session_user)):
|
361 |
+
"""Require a logged-in user, optionally requiring admin role"""
|
362 |
+
if not session_user:
|
363 |
+
raise HTTPException(
|
364 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
365 |
+
detail="Authentication required",
|
366 |
+
headers={"WWW-Authenticate": "Bearer"},
|
367 |
+
)
|
368 |
+
|
369 |
+
if admin_required and getattr(session_user, "role", "") != "admin":
|
370 |
+
raise HTTPException(
|
371 |
+
status_code=status.HTTP_403_FORBIDDEN,
|
372 |
+
detail="Admin access required",
|
373 |
+
)
|
374 |
+
|
375 |
+
return session_user
|
376 |
+
|
377 |
+
def verify_api_key(api_key: str = Depends(api_key_header),
|
378 |
+
credentials: HTTPAuthorizationCredentials = Depends(security)) -> UserInfo:
|
379 |
+
"""Verify API key from header or Bearer token"""
|
380 |
+
# Try to get the API key from the header
|
381 |
+
key = api_key
|
382 |
+
|
383 |
+
# If not found, try to get it from the Bearer token
|
384 |
+
if not key and credentials:
|
385 |
+
key = credentials.credentials
|
386 |
+
|
387 |
+
# Check if the key is valid
|
388 |
+
if not key or key not in API_KEYS:
|
389 |
+
raise HTTPException(
|
390 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
391 |
+
detail="Invalid API key",
|
392 |
+
headers={"WWW-Authenticate": "Bearer"},
|
393 |
+
)
|
394 |
+
|
395 |
+
return API_KEYS[key]["user"]
|
396 |
+
|
397 |
+
# Credit packages configuration
|
398 |
+
CREDIT_PACKAGES = [
|
399 |
+
{
|
400 |
+
"id": "starter",
|
401 |
+
"name": "Starter Pack",
|
402 |
+
"description": "Perfect for trying out our AI tools",
|
403 |
+
"credits": 100,
|
404 |
+
"price": 49.99
|
405 |
+
},
|
406 |
+
{
|
407 |
+
"id": "pro",
|
408 |
+
"name": "Pro Pack",
|
409 |
+
"description": "Most popular choice for regular users",
|
410 |
+
"credits": 500,
|
411 |
+
"price": 199.99
|
412 |
+
},
|
413 |
+
{
|
414 |
+
"id": "enterprise",
|
415 |
+
"name": "Enterprise Pack",
|
416 |
+
"description": "Best value for power users",
|
417 |
+
"credits": 2000,
|
418 |
+
"price": 690.99
|
419 |
+
}
|
420 |
+
]
|
421 |
+
|
422 |
+
# Mock user data (replace with database in production)
|
423 |
+
users = {}
|
424 |
+
|
425 |
+
# Routes
|
426 |
+
@app.get("/", response_class=HTMLResponse)
|
427 |
+
async def home(request: Request, session_user: Optional[UserInfo] = Depends(get_session_user)):
|
428 |
+
"""Home page with tools list"""
|
429 |
+
return templates.TemplateResponse(
|
430 |
+
"index.html",
|
431 |
+
{
|
432 |
+
"request": request,
|
433 |
+
"app_name": "AI Tool Hub",
|
434 |
+
"tools": TOOLS,
|
435 |
+
"session_user": session_user,
|
436 |
+
"user": session_user, # Add user for consistency with other templates
|
437 |
+
"user_credits": session_user.credits if session_user else 0
|
438 |
+
}
|
439 |
+
)
|
440 |
+
|
441 |
+
@app.get("/tool/{tool_id}", response_class=HTMLResponse)
|
442 |
+
async def tool_page(
|
443 |
+
tool_id: str,
|
444 |
+
request: Request,
|
445 |
+
session_user: Optional[UserInfo] = Depends(get_session_user)
|
446 |
+
):
|
447 |
+
"""Tool-specific page to execute a specific AI tool"""
|
448 |
+
# Get the tool
|
449 |
+
tool = next((t for t in TOOLS if t.id == tool_id), None)
|
450 |
+
|
451 |
+
if not tool:
|
452 |
+
return templates.TemplateResponse(
|
453 |
+
"error.html",
|
454 |
+
{
|
455 |
+
"request": request,
|
456 |
+
"app_name": "AI Tool Hub",
|
457 |
+
"error_title": "Tool Not Found",
|
458 |
+
"error_description": "The requested tool does not exist.",
|
459 |
+
"user": session_user,
|
460 |
+
"user_credits": session_user.credits if session_user else 0,
|
461 |
+
"tools": TOOLS # Include for consistent sidebar navigation
|
462 |
+
}
|
463 |
+
)
|
464 |
+
|
465 |
+
# Get example prompts for this tool
|
466 |
+
suggestions = tool.example_prompts if hasattr(tool, 'example_prompts') else []
|
467 |
+
|
468 |
+
# Check if the user has enough credits
|
469 |
+
has_enough_credits = session_user and session_user.credits >= tool.cost if session_user else False
|
470 |
+
|
471 |
+
# Get available providers directly from the tool
|
472 |
+
providers = tool.providers
|
473 |
+
|
474 |
+
return templates.TemplateResponse(
|
475 |
+
"tool.html",
|
476 |
+
{
|
477 |
+
"request": request,
|
478 |
+
"app_name": "AI Tool Hub",
|
479 |
+
"tool": tool,
|
480 |
+
"user": session_user,
|
481 |
+
"user_credits": session_user.credits if session_user else 0,
|
482 |
+
"has_enough_credits": has_enough_credits,
|
483 |
+
"suggestions": suggestions,
|
484 |
+
"providers": providers,
|
485 |
+
"tools": TOOLS # Include all tools for sidebar navigation
|
486 |
+
}
|
487 |
+
)
|
488 |
+
|
489 |
+
@app.get("/result/{result_id}", response_class=HTMLResponse)
|
490 |
+
async def result_page(
|
491 |
+
request: Request,
|
492 |
+
result_id: str,
|
493 |
+
session_user: Optional[UserInfo] = Depends(get_session_user)
|
494 |
+
):
|
495 |
+
"""Result display page"""
|
496 |
+
# In a real app, you'd fetch the result from a database
|
497 |
+
# Here, we'll use a mock result for demonstration
|
498 |
+
result = {
|
499 |
+
"id": result_id,
|
500 |
+
"type": "text", # or "image" or "code"
|
501 |
+
"provider": "openai",
|
502 |
+
"model": "gpt-4",
|
503 |
+
"prompt": "Write a short poem about AI",
|
504 |
+
"result": """
|
505 |
+
Silicon dreams in neural space,
|
506 |
+
Learning patterns, quickening pace.
|
507 |
+
Algorithms dance with grace divine,
|
508 |
+
In zeros and ones, intelligence shine.
|
509 |
+
|
510 |
+
Human-made yet growing beyond,
|
511 |
+
Of our creations, we grow fond.
|
512 |
+
Partners in progress, AI and we,
|
513 |
+
Crafting together what's yet to be.
|
514 |
+
""",
|
515 |
+
"created_at": datetime.now().isoformat(),
|
516 |
+
"response_time": 2.3 # seconds
|
517 |
+
}
|
518 |
+
|
519 |
+
return templates.TemplateResponse(
|
520 |
+
"result.html",
|
521 |
+
{
|
522 |
+
"request": request,
|
523 |
+
"app_name": "AI Tool Hub",
|
524 |
+
"result": result,
|
525 |
+
"user": session_user,
|
526 |
+
"user_credits": session_user.credits if session_user else 0
|
527 |
+
}
|
528 |
+
)
|
529 |
+
|
530 |
+
@app.get("/marketplace", response_class=HTMLResponse)
|
531 |
+
async def marketplace_page(
|
532 |
+
request: Request,
|
533 |
+
category: Optional[str] = None,
|
534 |
+
sort_by: str = "popular",
|
535 |
+
session_user: Optional[UserInfo] = Depends(get_session_user)
|
536 |
+
):
|
537 |
+
"""Prompt marketplace page"""
|
538 |
+
prompts = marketplace.list_marketplace_prompts(category=category, sort_by=sort_by)
|
539 |
+
|
540 |
+
return templates.TemplateResponse(
|
541 |
+
"marketplace.html",
|
542 |
+
{
|
543 |
+
"request": request,
|
544 |
+
"app_name": "AI Tool Hub",
|
545 |
+
"prompts": prompts,
|
546 |
+
"category": category,
|
547 |
+
"sort_by": sort_by,
|
548 |
+
"user": session_user,
|
549 |
+
"user_credits": session_user.credits if session_user else 0
|
550 |
+
}
|
551 |
+
)
|
552 |
+
|
553 |
+
@app.get("/ad", response_class=HTMLResponse)
|
554 |
+
async def ad_page(
|
555 |
+
request: Request,
|
556 |
+
session_user: Optional[UserInfo] = Depends(get_session_user)
|
557 |
+
):
|
558 |
+
"""Ad reward page"""
|
559 |
+
return templates.TemplateResponse(
|
560 |
+
"ad_reward.html",
|
561 |
+
{
|
562 |
+
"request": request,
|
563 |
+
"app_name": "AI Tool Hub",
|
564 |
+
"user": session_user,
|
565 |
+
"user_credits": session_user.credits if session_user else 0
|
566 |
+
}
|
567 |
+
)
|
568 |
+
|
569 |
+
@app.get("/login", response_class=HTMLResponse)
|
570 |
+
async def login_page(request: Request):
|
571 |
+
"""Login page"""
|
572 |
+
return templates.TemplateResponse(
|
573 |
+
"login.html",
|
574 |
+
{
|
575 |
+
"request": request,
|
576 |
+
"app_name": "AI Tool Hub"
|
577 |
+
}
|
578 |
+
)
|
579 |
+
|
580 |
+
@app.post("/login")
|
581 |
+
async def login(
|
582 |
+
request: Request,
|
583 |
+
username: str = Form(...),
|
584 |
+
password: str = Form(...)
|
585 |
+
):
|
586 |
+
"""Process login"""
|
587 |
+
# Check if admin login
|
588 |
+
if username == "admin":
|
589 |
+
# Check admin credentials
|
590 |
+
admin_key = API_KEYS.get("admin_key")
|
591 |
+
if not admin_key or admin_key.get("password") != password:
|
592 |
+
# Admin login failure
|
593 |
+
return templates.TemplateResponse(
|
594 |
+
"login.html",
|
595 |
+
{
|
596 |
+
"request": request,
|
597 |
+
"app_name": "AI Tool Hub",
|
598 |
+
"error": "Invalid admin credentials"
|
599 |
+
},
|
600 |
+
status_code=400
|
601 |
+
)
|
602 |
+
|
603 |
+
# Admin login success
|
604 |
+
user = admin_key["user"]
|
605 |
+
else:
|
606 |
+
# For demo purposes, any username/password combo works
|
607 |
+
# In production, you'd verify against a database
|
608 |
+
|
609 |
+
# Create or update user
|
610 |
+
user_id = f"user_{username.lower().replace(' ', '_')}"
|
611 |
+
user = UserInfo(
|
612 |
+
id=user_id,
|
613 |
+
username=username,
|
614 |
+
email=f"{username.lower().replace(' ', '.')}@example.com",
|
615 |
+
last_login=datetime.now().isoformat()
|
616 |
+
)
|
617 |
+
|
618 |
+
# Create session
|
619 |
+
session_id = secrets.token_urlsafe(32)
|
620 |
+
SESSIONS[session_id] = {
|
621 |
+
"user": user,
|
622 |
+
"expires": datetime.now() + timedelta(hours=24)
|
623 |
+
}
|
624 |
+
|
625 |
+
# Create redirect response
|
626 |
+
response = RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)
|
627 |
+
|
628 |
+
# Set session cookie
|
629 |
+
response.set_cookie(
|
630 |
+
key="session_id",
|
631 |
+
value=session_id,
|
632 |
+
max_age=86400, # 24 hours
|
633 |
+
httponly=True,
|
634 |
+
samesite="lax"
|
635 |
+
)
|
636 |
+
|
637 |
+
return response
|
638 |
+
|
639 |
+
@app.get("/logout")
|
640 |
+
async def logout(
|
641 |
+
response: Response,
|
642 |
+
session_id: Optional[str] = Cookie(None)
|
643 |
+
):
|
644 |
+
"""Process logout"""
|
645 |
+
if session_id and session_id in SESSIONS:
|
646 |
+
del SESSIONS[session_id]
|
647 |
+
|
648 |
+
response.delete_cookie(key="session_id")
|
649 |
+
|
650 |
+
return RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)
|
651 |
+
|
652 |
+
@app.get("/credits", response_class=HTMLResponse)
|
653 |
+
async def credits_page(request: Request, session_user: Optional[UserInfo] = Depends(get_session_user)):
|
654 |
+
"""Credits management page"""
|
655 |
+
if not session_user:
|
656 |
+
return RedirectResponse(url="/login")
|
657 |
+
|
658 |
+
# Get credit history (mock data - replace with database in production)
|
659 |
+
credit_history = [
|
660 |
+
{
|
661 |
+
"date": "2024-03-20 14:30",
|
662 |
+
"type": "Daily Reward",
|
663 |
+
"amount": 5,
|
664 |
+
"details": "Watched daily ad"
|
665 |
+
},
|
666 |
+
{
|
667 |
+
"date": "2024-03-19 15:45",
|
668 |
+
"type": "Tool Usage",
|
669 |
+
"amount": -5,
|
670 |
+
"details": "Used Image Generation"
|
671 |
+
}
|
672 |
+
]
|
673 |
+
|
674 |
+
return templates.TemplateResponse(
|
675 |
+
"credits.html",
|
676 |
+
{
|
677 |
+
"request": request,
|
678 |
+
"app_name": "AI Tool Hub",
|
679 |
+
"user": session_user,
|
680 |
+
"user_credits": session_user.credits,
|
681 |
+
"credit_packages": CREDIT_PACKAGES,
|
682 |
+
"credit_history": credit_history,
|
683 |
+
"referral_link": f"{request.base_url}?ref={session_user.id}"
|
684 |
+
}
|
685 |
+
)
|
686 |
+
|
687 |
+
@app.get("/api/ads/status")
|
688 |
+
async def get_ads_status(request: Request, session_user: Optional[UserInfo] = Depends(get_session_user)):
|
689 |
+
"""Get user's ad reward status"""
|
690 |
+
if not session_user:
|
691 |
+
return JSONResponse(
|
692 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
693 |
+
content={"success": False, "error": "Authentication required"}
|
694 |
+
)
|
695 |
+
|
696 |
+
# Get daily reward status
|
697 |
+
daily_reward = DAILY_REWARDS.get(f"{session_user.id}_{datetime.now().date().isoformat()}")
|
698 |
+
can_claim_daily = True
|
699 |
+
cooldown_remaining = 0
|
700 |
+
|
701 |
+
if daily_reward and daily_reward.last_reward:
|
702 |
+
last_reward_time = datetime.fromisoformat(daily_reward.last_reward)
|
703 |
+
if (datetime.now() - last_reward_time).total_seconds() < 3600: # 1 hour cooldown
|
704 |
+
can_claim_daily = False
|
705 |
+
cooldown_remaining = 3600 - (datetime.now() - last_reward_time).total_seconds()
|
706 |
+
|
707 |
+
# Get special reward status
|
708 |
+
special_reward = SPECIAL_REWARDS.get(f"{session_user.id}_{datetime.now().date().isoformat()}")
|
709 |
+
can_claim_special = True
|
710 |
+
|
711 |
+
if special_reward and special_reward.claimed:
|
712 |
+
can_claim_special = False
|
713 |
+
|
714 |
+
return JSONResponse(
|
715 |
+
content={
|
716 |
+
"success": True,
|
717 |
+
"daily_rewards": {
|
718 |
+
"count": daily_reward.count if daily_reward else 0,
|
719 |
+
"can_claim": can_claim_daily and (not daily_reward or daily_reward.count < 3),
|
720 |
+
"cooldown_remaining": int(cooldown_remaining)
|
721 |
+
},
|
722 |
+
"special_reward": {
|
723 |
+
"claimed": special_reward.claimed if special_reward else False,
|
724 |
+
"can_claim": can_claim_special
|
725 |
+
}
|
726 |
+
}
|
727 |
+
)
|
728 |
+
|
729 |
+
@app.post("/api/ads/claim")
|
730 |
+
async def claim_ad_reward(
|
731 |
+
request: Request,
|
732 |
+
session_user: Optional[UserInfo] = Depends(get_session_user)
|
733 |
+
):
|
734 |
+
"""Process ad reward claim"""
|
735 |
+
if not session_user:
|
736 |
+
return JSONResponse(
|
737 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
738 |
+
content={"success": False, "error": "Authentication required"}
|
739 |
+
)
|
740 |
+
|
741 |
+
data = await request.json()
|
742 |
+
reward_type = data.get("type")
|
743 |
+
|
744 |
+
if reward_type not in ["daily", "special"]:
|
745 |
+
return JSONResponse(
|
746 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
747 |
+
content={"success": False, "error": "Invalid reward type"}
|
748 |
+
)
|
749 |
+
|
750 |
+
today = datetime.now().date().isoformat()
|
751 |
+
|
752 |
+
if reward_type == "daily":
|
753 |
+
# Get or create daily reward record
|
754 |
+
daily_key = f"{session_user.id}_{today}"
|
755 |
+
if daily_key not in DAILY_REWARDS:
|
756 |
+
DAILY_REWARDS[daily_key] = DailyReward(
|
757 |
+
user_id=session_user.id,
|
758 |
+
date=today,
|
759 |
+
count=0
|
760 |
+
)
|
761 |
+
|
762 |
+
daily_reward = DAILY_REWARDS[daily_key]
|
763 |
+
|
764 |
+
# Check if user has already claimed maximum daily rewards
|
765 |
+
if daily_reward.count >= 3:
|
766 |
+
return JSONResponse(
|
767 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
768 |
+
content={"success": False, "error": "Daily reward limit reached"}
|
769 |
+
)
|
770 |
+
|
771 |
+
# Check cooldown period
|
772 |
+
if daily_reward.last_reward:
|
773 |
+
last_reward_time = datetime.fromisoformat(daily_reward.last_reward)
|
774 |
+
if (datetime.now() - last_reward_time).total_seconds() < 3600:
|
775 |
+
return JSONResponse(
|
776 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
777 |
+
content={"success": False, "error": "Please wait 1 hour between rewards"}
|
778 |
+
)
|
779 |
+
|
780 |
+
# Award credits
|
781 |
+
reward_amount = 10
|
782 |
+
session_user.credits += reward_amount
|
783 |
+
daily_reward.count += 1
|
784 |
+
daily_reward.last_reward = datetime.now().isoformat()
|
785 |
+
|
786 |
+
return JSONResponse(
|
787 |
+
content={
|
788 |
+
"success": True,
|
789 |
+
"credits_earned": reward_amount,
|
790 |
+
"current_credits": session_user.credits,
|
791 |
+
"daily_progress": daily_reward.count
|
792 |
+
}
|
793 |
+
)
|
794 |
+
|
795 |
+
else: # special reward
|
796 |
+
# Get or create special reward record
|
797 |
+
special_key = f"{session_user.id}_{today}"
|
798 |
+
if special_key not in SPECIAL_REWARDS:
|
799 |
+
SPECIAL_REWARDS[special_key] = SpecialReward(
|
800 |
+
user_id=session_user.id,
|
801 |
+
date=today,
|
802 |
+
claimed=False
|
803 |
+
)
|
804 |
+
|
805 |
+
special_reward = SPECIAL_REWARDS[special_key]
|
806 |
+
|
807 |
+
# Check if user has already claimed special reward
|
808 |
+
if special_reward.claimed:
|
809 |
+
return JSONResponse(
|
810 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
811 |
+
content={"success": False, "error": "Special reward already claimed"}
|
812 |
+
)
|
813 |
+
|
814 |
+
# Award credits
|
815 |
+
reward_amount = 25
|
816 |
+
session_user.credits += reward_amount
|
817 |
+
special_reward.claimed = True
|
818 |
+
special_reward.claimed_at = datetime.now().isoformat()
|
819 |
+
|
820 |
+
return JSONResponse(
|
821 |
+
content={
|
822 |
+
"success": True,
|
823 |
+
"credits_earned": reward_amount,
|
824 |
+
"current_credits": session_user.credits
|
825 |
+
}
|
826 |
+
)
|
827 |
+
|
828 |
+
@app.get("/create-admin")
|
829 |
+
async def create_admin(
|
830 |
+
request: Request,
|
831 |
+
response: Response,
|
832 |
+
):
|
833 |
+
"""Create an admin user (only for development)"""
|
834 |
+
# Check if admin already exists
|
835 |
+
admin_exists = False
|
836 |
+
admin_user = None
|
837 |
+
|
838 |
+
for session in SESSIONS.values():
|
839 |
+
if session.get("user") and session["user"].username == "admin":
|
840 |
+
admin_exists = True
|
841 |
+
admin_user = session["user"]
|
842 |
+
break
|
843 |
+
|
844 |
+
if admin_exists:
|
845 |
+
# Update admin credits
|
846 |
+
admin_user.credits = 10000
|
847 |
+
admin_user.role = "admin"
|
848 |
+
return JSONResponse(
|
849 |
+
content={
|
850 |
+
"success": True,
|
851 |
+
"message": "Admin user updated with 10000 credits",
|
852 |
+
"user_id": admin_user.id
|
853 |
+
}
|
854 |
+
)
|
855 |
+
|
856 |
+
# Create admin user
|
857 |
+
user_id = "admin"
|
858 |
+
user = UserInfo(
|
859 |
+
id=user_id,
|
860 |
+
username="admin",
|
861 |
+
email="[email protected]",
|
862 |
+
credits=10000,
|
863 |
+
role="admin",
|
864 |
+
last_login=datetime.now().isoformat()
|
865 |
+
)
|
866 |
+
|
867 |
+
# In production, you would hash the password
|
868 |
+
# For simplicity, we'll store it in a way that the login function can use it
|
869 |
+
# This is for development only, never do this in production!
|
870 |
+
admin_info = {
|
871 |
+
"user": user,
|
872 |
+
"password": "admin123", # Plaintext for demo only
|
873 |
+
"created_at": datetime.now().isoformat()
|
874 |
+
}
|
875 |
+
|
876 |
+
# Store admin info for login
|
877 |
+
API_KEYS["admin_key"] = admin_info
|
878 |
+
|
879 |
+
return JSONResponse(
|
880 |
+
content={
|
881 |
+
"success": True,
|
882 |
+
"message": "Admin user created with 10000 credits",
|
883 |
+
"user_id": user_id,
|
884 |
+
"username": "admin",
|
885 |
+
"password": "admin123"
|
886 |
+
}
|
887 |
+
)
|
888 |
+
|
889 |
+
@app.get("/admin", response_class=HTMLResponse)
|
890 |
+
async def admin_dashboard(
|
891 |
+
request: Request,
|
892 |
+
user: UserInfo = Depends(lambda: require_session_user(admin_required=True))
|
893 |
+
):
|
894 |
+
"""Admin dashboard"""
|
895 |
+
# Get all users
|
896 |
+
users = []
|
897 |
+
for session_id, session in SESSIONS.items():
|
898 |
+
if "user" in session:
|
899 |
+
users.append({
|
900 |
+
"id": session["user"].id,
|
901 |
+
"username": session["user"].username,
|
902 |
+
"email": session["user"].email,
|
903 |
+
"credits": session["user"].credits,
|
904 |
+
"role": getattr(session["user"], "role", "user"),
|
905 |
+
"session_id": session_id,
|
906 |
+
"expires": session["expires"].isoformat() if "expires" in session else None
|
907 |
+
})
|
908 |
+
|
909 |
+
# Sort users by credits (descending)
|
910 |
+
users.sort(key=lambda x: x["credits"], reverse=True)
|
911 |
+
|
912 |
+
return templates.TemplateResponse(
|
913 |
+
"admin_dashboard.html",
|
914 |
+
{
|
915 |
+
"request": request,
|
916 |
+
"app_name": "AI Tool Hub",
|
917 |
+
"user": user,
|
918 |
+
"user_credits": user.credits,
|
919 |
+
"users": users,
|
920 |
+
"total_users": len(users),
|
921 |
+
"total_credits": sum(u["credits"] for u in users),
|
922 |
+
"admin_count": sum(1 for u in users if u["role"] == "admin")
|
923 |
+
}
|
924 |
+
)
|
925 |
+
|
926 |
+
@app.post("/api/admin/update-credits")
|
927 |
+
async def admin_update_credits(
|
928 |
+
request: Request,
|
929 |
+
user: UserInfo = Depends(lambda: require_session_user(admin_required=True))
|
930 |
+
):
|
931 |
+
"""Admin endpoint to update user credits"""
|
932 |
+
data = await request.json()
|
933 |
+
user_id = data.get("user_id")
|
934 |
+
new_credits = data.get("credits")
|
935 |
+
|
936 |
+
if not user_id or new_credits is None:
|
937 |
+
return JSONResponse(
|
938 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
939 |
+
content={"success": False, "error": "User ID and credits are required"}
|
940 |
+
)
|
941 |
+
|
942 |
+
# Find the user
|
943 |
+
target_user = None
|
944 |
+
for session in SESSIONS.values():
|
945 |
+
if "user" in session and session["user"].id == user_id:
|
946 |
+
target_user = session["user"]
|
947 |
+
break
|
948 |
+
|
949 |
+
if not target_user:
|
950 |
+
return JSONResponse(
|
951 |
+
status_code=status.HTTP_404_NOT_FOUND,
|
952 |
+
content={"success": False, "error": "User not found"}
|
953 |
+
)
|
954 |
+
|
955 |
+
# Update credits
|
956 |
+
try:
|
957 |
+
target_user.credits = float(new_credits)
|
958 |
+
except ValueError:
|
959 |
+
return JSONResponse(
|
960 |
+
status_code=status.HTTP_400_BAD_REQUEST,
|
961 |
+
content={"success": False, "error": "Invalid credit amount"}
|
962 |
+
)
|
963 |
+
|
964 |
+
return JSONResponse(
|
965 |
+
content={
|
966 |
+
"success": True,
|
967 |
+
"user_id": user_id,
|
968 |
+
"new_credits": target_user.credits,
|
969 |
+
"message": f"Credits updated for user {target_user.username}"
|
970 |
+
}
|
971 |
+
)
|
972 |
+
|
973 |
+
@app.post("/process-request", response_class=HTMLResponse)
|
974 |
+
async def process_request(
|
975 |
+
request: Request,
|
976 |
+
tool_id: str = Form(...),
|
977 |
+
prompt: str = Form(...),
|
978 |
+
provider: str = Form("openai"),
|
979 |
+
model: str = Form("default"),
|
980 |
+
session_user: Optional[UserInfo] = Depends(get_session_user)
|
981 |
+
):
|
982 |
+
"""Process a tool request from a form"""
|
983 |
+
# Get the tool
|
984 |
+
tool = next((t for t in TOOLS if t.id == tool_id), None)
|
985 |
+
|
986 |
+
if not tool:
|
987 |
+
return templates.TemplateResponse(
|
988 |
+
"error.html",
|
989 |
+
{
|
990 |
+
"request": request,
|
991 |
+
"app_name": "AI Tool Hub",
|
992 |
+
"error_title": "Tool Not Found",
|
993 |
+
"error_description": "The requested tool does not exist.",
|
994 |
+
"user": session_user,
|
995 |
+
"user_credits": session_user.credits if session_user else 0,
|
996 |
+
"tools": TOOLS # Include for consistent sidebar navigation
|
997 |
+
}
|
998 |
+
)
|
999 |
+
|
1000 |
+
# Confirm that user is logged in
|
1001 |
+
if not session_user:
|
1002 |
+
return templates.TemplateResponse(
|
1003 |
+
"error.html",
|
1004 |
+
{
|
1005 |
+
"request": request,
|
1006 |
+
"app_name": "AI Tool Hub",
|
1007 |
+
"error_title": "Login Required",
|
1008 |
+
"error_description": "You must be logged in to use AI tools.",
|
1009 |
+
"user": None,
|
1010 |
+
"user_credits": 0,
|
1011 |
+
"tools": TOOLS # Include for consistent sidebar navigation
|
1012 |
+
}
|
1013 |
+
)
|
1014 |
+
|
1015 |
+
# Check if the user has enough credits
|
1016 |
+
if session_user.credits < tool.cost:
|
1017 |
+
return templates.TemplateResponse(
|
1018 |
+
"ad_reward.html",
|
1019 |
+
{
|
1020 |
+
"request": request,
|
1021 |
+
"app_name": "AI Tool Hub",
|
1022 |
+
"user": session_user,
|
1023 |
+
"user_credits": session_user.credits,
|
1024 |
+
"tool": tool,
|
1025 |
+
"reward_ad": ads_manager.get_ad_code("reward_video"),
|
1026 |
+
"sidebar_ad": ads_manager.get_ad_code("sidebar"),
|
1027 |
+
"return_url": f"/tool/{tool_id}",
|
1028 |
+
"tools": TOOLS # Include for consistent sidebar navigation
|
1029 |
+
}
|
1030 |
+
)
|
1031 |
+
|
1032 |
+
# Get the provider
|
1033 |
+
try:
|
1034 |
+
provider_instance = get_provider(provider)
|
1035 |
+
if not provider_instance:
|
1036 |
+
return templates.TemplateResponse(
|
1037 |
+
"error.html",
|
1038 |
+
{
|
1039 |
+
"request": request,
|
1040 |
+
"app_name": "AI Tool Hub",
|
1041 |
+
"error_title": "Provider Not Supported",
|
1042 |
+
"error_description": f"The provider '{provider}' is not supported.",
|
1043 |
+
"user": session_user,
|
1044 |
+
"user_credits": session_user.credits if session_user else 0,
|
1045 |
+
"tools": TOOLS # Include for consistent sidebar navigation
|
1046 |
+
}
|
1047 |
+
)
|
1048 |
+
|
1049 |
+
result = None
|
1050 |
+
result_type = "text" # Default type
|
1051 |
+
|
1052 |
+
# Determine result type based on tool ID
|
1053 |
+
if "image" in tool.id or "logo" in tool.id or "avatar" in tool.id:
|
1054 |
+
result_type = "image"
|
1055 |
+
elif "code" in tool.id or "debugging" in tool.id:
|
1056 |
+
result_type = "code"
|
1057 |
+
|
1058 |
+
# Process the request based on tool type
|
1059 |
+
try:
|
1060 |
+
if result_type == "text":
|
1061 |
+
# Text generation tool
|
1062 |
+
result = provider_instance.generate_text(
|
1063 |
+
prompt=prompt,
|
1064 |
+
model=model if model != "default" else "mistralai/Mistral-7B-Instruct-v0.2",
|
1065 |
+
max_tokens=1000,
|
1066 |
+
temperature=0.7
|
1067 |
+
)
|
1068 |
+
elif result_type == "image":
|
1069 |
+
# Image generation tool
|
1070 |
+
try:
|
1071 |
+
result = provider_instance.generate_image(
|
1072 |
+
prompt=prompt,
|
1073 |
+
model=model if model != "default" else "stabilityai/stable-diffusion-xl-base-1.0"
|
1074 |
+
)
|
1075 |
+
except AttributeError:
|
1076 |
+
# Provider doesn't support image generation
|
1077 |
+
return templates.TemplateResponse(
|
1078 |
+
"error.html",
|
1079 |
+
{
|
1080 |
+
"request": request,
|
1081 |
+
"app_name": "AI Tool Hub",
|
1082 |
+
"error_title": "Provider Not Supported",
|
1083 |
+
"error_description": f"The provider '{provider}' does not support image generation.",
|
1084 |
+
"user": session_user,
|
1085 |
+
"user_credits": session_user.credits if session_user else 0,
|
1086 |
+
"tools": TOOLS # Include for consistent sidebar navigation
|
1087 |
+
}
|
1088 |
+
)
|
1089 |
+
elif result_type == "code":
|
1090 |
+
# Code generation tool
|
1091 |
+
result = provider_instance.generate_text(
|
1092 |
+
prompt=prompt,
|
1093 |
+
model=model if model != "default" else "gpt2",
|
1094 |
+
max_tokens=1500,
|
1095 |
+
temperature=0.5
|
1096 |
+
)
|
1097 |
+
else:
|
1098 |
+
return templates.TemplateResponse(
|
1099 |
+
"error.html",
|
1100 |
+
{
|
1101 |
+
"request": request,
|
1102 |
+
"app_name": "AI Tool Hub",
|
1103 |
+
"error_title": "Tool Type Not Supported",
|
1104 |
+
"error_description": f"The tool type '{result_type}' is not supported.",
|
1105 |
+
"user": session_user,
|
1106 |
+
"user_credits": session_user.credits if session_user else 0,
|
1107 |
+
"tools": TOOLS # Include for consistent sidebar navigation
|
1108 |
+
}
|
1109 |
+
)
|
1110 |
+
|
1111 |
+
# Deduct credits
|
1112 |
+
session_user.credits -= tool.cost
|
1113 |
+
|
1114 |
+
# Return the result
|
1115 |
+
return templates.TemplateResponse(
|
1116 |
+
"result.html",
|
1117 |
+
{
|
1118 |
+
"request": request,
|
1119 |
+
"app_name": "AI Tool Hub",
|
1120 |
+
"user": session_user,
|
1121 |
+
"user_credits": session_user.credits,
|
1122 |
+
"tool": tool,
|
1123 |
+
"prompt": prompt,
|
1124 |
+
"result": result,
|
1125 |
+
"result_type": result_type,
|
1126 |
+
"tools": TOOLS, # Include all tools for sidebar navigation
|
1127 |
+
"json_data": result.get("raw_response", {}) if isinstance(result, dict) else {}
|
1128 |
+
}
|
1129 |
+
)
|
1130 |
+
except Exception as generate_error:
|
1131 |
+
return templates.TemplateResponse(
|
1132 |
+
"error.html",
|
1133 |
+
{
|
1134 |
+
"request": request,
|
1135 |
+
"app_name": "AI Tool Hub",
|
1136 |
+
"error_title": "Generation Error",
|
1137 |
+
"error_description": f"Error generating content: {str(generate_error)}",
|
1138 |
+
"user": session_user,
|
1139 |
+
"user_credits": session_user.credits,
|
1140 |
+
"tools": TOOLS # Include all tools for sidebar navigation
|
1141 |
+
}
|
1142 |
+
)
|
1143 |
+
except Exception as provider_error:
|
1144 |
+
return templates.TemplateResponse(
|
1145 |
+
"error.html",
|
1146 |
+
{
|
1147 |
+
"request": request,
|
1148 |
+
"app_name": "AI Tool Hub",
|
1149 |
+
"error_title": "Provider Error",
|
1150 |
+
"error_description": f"Error with provider: {str(provider_error)}",
|
1151 |
+
"user": session_user,
|
1152 |
+
"user_credits": session_user.credits,
|
1153 |
+
"tools": TOOLS # Include all tools for sidebar navigation
|
1154 |
+
}
|
1155 |
+
)
|
1156 |
+
|
1157 |
+
@app.post("/register")
|
1158 |
+
async def register(
|
1159 |
+
request: Request,
|
1160 |
+
response: Response,
|
1161 |
+
username: str = Form(...),
|
1162 |
+
email: str = Form(...),
|
1163 |
+
password: str = Form(...),
|
1164 |
+
session_id: Optional[str] = Cookie(None)
|
1165 |
+
):
|
1166 |
+
"""Process registration"""
|
1167 |
+
# Check if username or email already exists (in production use a database)
|
1168 |
+
for session in SESSIONS.values():
|
1169 |
+
if session.get("user"):
|
1170 |
+
if session["user"].username.lower() == username.lower():
|
1171 |
+
return templates.TemplateResponse(
|
1172 |
+
"register.html",
|
1173 |
+
{
|
1174 |
+
"request": request,
|
1175 |
+
"app_name": "AI Tool Hub",
|
1176 |
+
"error": "Username already exists",
|
1177 |
+
"form_data": {"username": username, "email": email}
|
1178 |
+
},
|
1179 |
+
status_code=400
|
1180 |
+
)
|
1181 |
+
if session["user"].email.lower() == email.lower():
|
1182 |
+
return templates.TemplateResponse(
|
1183 |
+
"register.html",
|
1184 |
+
{
|
1185 |
+
"request": request,
|
1186 |
+
"app_name": "AI Tool Hub",
|
1187 |
+
"error": "Email already exists",
|
1188 |
+
"form_data": {"username": username, "email": email}
|
1189 |
+
},
|
1190 |
+
status_code=400
|
1191 |
+
)
|
1192 |
+
|
1193 |
+
# Create user
|
1194 |
+
user_id = f"user_{username.lower().replace(' ', '_')}"
|
1195 |
+
|
1196 |
+
# Check if there's an existing temporary session to convert
|
1197 |
+
is_converting_temp = False
|
1198 |
+
temp_credits = 0
|
1199 |
+
|
1200 |
+
if session_id and session_id in SESSIONS:
|
1201 |
+
session = SESSIONS[session_id]
|
1202 |
+
if session.get("is_temporary", False):
|
1203 |
+
# Convert temporary session to permanent
|
1204 |
+
is_converting_temp = True
|
1205 |
+
temp_credits = session["user"].credits
|
1206 |
+
# Delete the temporary session
|
1207 |
+
del SESSIONS[session_id]
|
1208 |
+
|
1209 |
+
# Create new user with starting credits (more if converting from temp)
|
1210 |
+
start_credits = 10.0 + temp_credits
|
1211 |
+
user = UserInfo(
|
1212 |
+
id=user_id,
|
1213 |
+
username=username,
|
1214 |
+
email=email,
|
1215 |
+
credits=start_credits,
|
1216 |
+
last_login=datetime.now().isoformat()
|
1217 |
+
)
|
1218 |
+
|
1219 |
+
# In production, you would hash the password and store in database
|
1220 |
+
|
1221 |
+
# Create new session
|
1222 |
+
new_session_id = secrets.token_urlsafe(32)
|
1223 |
+
SESSIONS[new_session_id] = {
|
1224 |
+
"user": user,
|
1225 |
+
"expires": datetime.now() + timedelta(hours=24)
|
1226 |
+
}
|
1227 |
+
|
1228 |
+
# Set session cookie
|
1229 |
+
response = RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER)
|
1230 |
+
response.set_cookie(
|
1231 |
+
key="session_id",
|
1232 |
+
value=new_session_id,
|
1233 |
+
max_age=86400, # 24 hours
|
1234 |
+
httponly=True,
|
1235 |
+
samesite="lax"
|
1236 |
+
)
|
1237 |
+
|
1238 |
+
return response
|
1239 |
+
|
1240 |
+
if __name__ == "__main__":
|
1241 |
+
import uvicorn
|
1242 |
+
uvicorn.run(app, host="0.0.0.0", port=8006)
|
magicAi.db
ADDED
Binary file (41 kB). View file
|
|
models.py
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from sqlalchemy import Column, String, Integer, Float
|
2 |
+
from sqlalchemy.ext.declarative import declarative_base
|
3 |
+
|
4 |
+
Base = declarative_base()
|
5 |
+
|
6 |
+
class User(Base):
|
7 |
+
__tablename__ = 'users'
|
8 |
+
|
9 |
+
id = Column(Integer, primary_key=True, index=True)
|
10 |
+
username = Column(String, unique=True, index=True)
|
11 |
+
email = Column(String, unique=True, index=True)
|
12 |
+
credits = Column(Float, default=10.0)
|
13 |
+
|
14 |
+
class Prompt(Base):
|
15 |
+
__tablename__ = 'prompts'
|
16 |
+
|
17 |
+
id = Column(String, primary_key=True, index=True)
|
18 |
+
title = Column(String, index=True)
|
19 |
+
content = Column(String)
|
20 |
+
description = Column(String, nullable=True)
|
21 |
+
creator_id = Column(String, index=True)
|
prompts/__init__.py
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Prompts Package
|
3 |
+
Exports prompt template and marketplace classes
|
4 |
+
"""
|
5 |
+
|
6 |
+
from prompts.prompt_templates import PromptTemplate, PromptTemplateManager
|
7 |
+
from prompts.prompt_marketplace import PromptMarketplace
|
8 |
+
|
9 |
+
__all__ = [
|
10 |
+
'PromptTemplate',
|
11 |
+
'PromptTemplateManager',
|
12 |
+
'PromptMarketplace'
|
13 |
+
]
|
prompts/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (407 Bytes). View file
|
|
prompts/__pycache__/prompt_marketplace.cpython-310.pyc
ADDED
Binary file (19.7 kB). View file
|
|
prompts/__pycache__/prompt_templates.cpython-310.pyc
ADDED
Binary file (10.9 kB). View file
|
|
prompts/marketplace_data/purchases.json
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
[]
|
prompts/marketplace_data/sales.json
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
[]
|
prompts/marketplace_data/stats.json
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"total_sales": 0,
|
3 |
+
"total_revenue": 0,
|
4 |
+
"prompt_usage": {},
|
5 |
+
"popular_categories": {}
|
6 |
+
}
|
prompts/prompt_marketplace.py
ADDED
@@ -0,0 +1,728 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Prompt Marketplace Module
|
3 |
+
Enables users to create, sell, purchase, and use AI prompts
|
4 |
+
"""
|
5 |
+
import os
|
6 |
+
import json
|
7 |
+
import uuid
|
8 |
+
import logging
|
9 |
+
from typing import Dict, Any, List, Optional
|
10 |
+
from datetime import datetime
|
11 |
+
|
12 |
+
from prompts.prompt_templates import PromptTemplate, PromptTemplateManager
|
13 |
+
|
14 |
+
# Setup logging
|
15 |
+
logging.basicConfig(level=logging.INFO)
|
16 |
+
logger = logging.getLogger("prompt_marketplace")
|
17 |
+
|
18 |
+
class PromptMarketplace:
|
19 |
+
"""Manages the prompt marketplace functionality"""
|
20 |
+
|
21 |
+
def __init__(self, data_dir: str = None):
|
22 |
+
"""Initialize the prompt marketplace"""
|
23 |
+
self.data_dir = data_dir or os.path.join(os.path.dirname(__file__), "marketplace_data")
|
24 |
+
|
25 |
+
# Create data directory if it doesn't exist
|
26 |
+
os.makedirs(self.data_dir, exist_ok=True)
|
27 |
+
|
28 |
+
# Paths for data files
|
29 |
+
self.purchases_file = os.path.join(self.data_dir, "purchases.json")
|
30 |
+
self.sales_file = os.path.join(self.data_dir, "sales.json")
|
31 |
+
self.stats_file = os.path.join(self.data_dir, "stats.json")
|
32 |
+
|
33 |
+
# Initialize data files if they don't exist
|
34 |
+
self._initialize_data_files()
|
35 |
+
|
36 |
+
# Create prompt template manager
|
37 |
+
self.template_manager = PromptTemplateManager()
|
38 |
+
|
39 |
+
def _initialize_data_files(self):
|
40 |
+
"""Initialize data files if they don't exist"""
|
41 |
+
if not os.path.exists(self.purchases_file):
|
42 |
+
with open(self.purchases_file, "w") as f:
|
43 |
+
json.dump([], f)
|
44 |
+
|
45 |
+
if not os.path.exists(self.sales_file):
|
46 |
+
with open(self.sales_file, "w") as f:
|
47 |
+
json.dump([], f)
|
48 |
+
|
49 |
+
if not os.path.exists(self.stats_file):
|
50 |
+
with open(self.stats_file, "w") as f:
|
51 |
+
json.dump({
|
52 |
+
"total_sales": 0,
|
53 |
+
"total_revenue": 0,
|
54 |
+
"prompt_usage": {},
|
55 |
+
"popular_categories": {}
|
56 |
+
}, f, indent=2)
|
57 |
+
|
58 |
+
def list_marketplace_prompts(self, category: str = None, sort_by: str = "popular") -> List[Dict[str, Any]]:
|
59 |
+
"""
|
60 |
+
List prompts available in the marketplace
|
61 |
+
|
62 |
+
Args:
|
63 |
+
category: Optional category to filter by
|
64 |
+
sort_by: Sorting method ('popular', 'newest', 'price_low', 'price_high')
|
65 |
+
|
66 |
+
Returns:
|
67 |
+
List of prompt templates available for purchase
|
68 |
+
"""
|
69 |
+
# Get all public templates
|
70 |
+
all_templates = self.template_manager.get_public_templates()
|
71 |
+
|
72 |
+
# Filter by category if specified
|
73 |
+
if category:
|
74 |
+
all_templates = [t for t in all_templates if t.category.lower() == category.lower()]
|
75 |
+
|
76 |
+
# Convert to dictionaries for easier manipulation
|
77 |
+
templates_dict = [t.to_dict() for t in all_templates]
|
78 |
+
|
79 |
+
# Add usage statistics
|
80 |
+
templates_with_stats = self._add_stats_to_templates(templates_dict)
|
81 |
+
|
82 |
+
# Sort the templates
|
83 |
+
if sort_by == "popular":
|
84 |
+
templates_with_stats.sort(key=lambda x: x.get("stats", {}).get("usage_count", 0), reverse=True)
|
85 |
+
elif sort_by == "newest":
|
86 |
+
templates_with_stats.sort(key=lambda x: x.get("created_at", ""), reverse=True)
|
87 |
+
elif sort_by == "price_low":
|
88 |
+
templates_with_stats.sort(key=lambda x: x.get("price", 0))
|
89 |
+
elif sort_by == "price_high":
|
90 |
+
templates_with_stats.sort(key=lambda x: x.get("price", 0), reverse=True)
|
91 |
+
|
92 |
+
return templates_with_stats
|
93 |
+
|
94 |
+
def _add_stats_to_templates(self, templates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
95 |
+
"""Add usage statistics to template dictionaries"""
|
96 |
+
try:
|
97 |
+
with open(self.stats_file, "r") as f:
|
98 |
+
stats = json.load(f)
|
99 |
+
|
100 |
+
prompt_usage = stats.get("prompt_usage", {})
|
101 |
+
|
102 |
+
# Add stats to each template
|
103 |
+
for template in templates:
|
104 |
+
template_id = template.get("id")
|
105 |
+
template["stats"] = {
|
106 |
+
"usage_count": prompt_usage.get(template_id, {}).get("count", 0),
|
107 |
+
"purchase_count": prompt_usage.get(template_id, {}).get("purchases", 0),
|
108 |
+
"rating": prompt_usage.get(template_id, {}).get("avg_rating", 0),
|
109 |
+
"reviews": prompt_usage.get(template_id, {}).get("review_count", 0)
|
110 |
+
}
|
111 |
+
|
112 |
+
return templates
|
113 |
+
except Exception as e:
|
114 |
+
logger.error(f"Error adding stats to templates: {e}")
|
115 |
+
return templates
|
116 |
+
|
117 |
+
def get_prompt_details(self, prompt_id: str) -> Dict[str, Any]:
|
118 |
+
"""
|
119 |
+
Get detailed information about a marketplace prompt
|
120 |
+
|
121 |
+
Args:
|
122 |
+
prompt_id: ID of the prompt template
|
123 |
+
|
124 |
+
Returns:
|
125 |
+
Detailed prompt information including stats and sample outputs
|
126 |
+
"""
|
127 |
+
prompt = self.template_manager.get_template(prompt_id)
|
128 |
+
|
129 |
+
if not prompt:
|
130 |
+
return {"success": False, "error": "Prompt not found"}
|
131 |
+
|
132 |
+
if not prompt.is_public and prompt.created_by != "system":
|
133 |
+
return {"success": False, "error": "Prompt is not available in the marketplace"}
|
134 |
+
|
135 |
+
# Get prompt details
|
136 |
+
prompt_dict = prompt.to_dict()
|
137 |
+
|
138 |
+
# Add stats
|
139 |
+
prompt_dict = self._add_stats_to_templates([prompt_dict])[0]
|
140 |
+
|
141 |
+
# Get purchase information
|
142 |
+
prompt_dict["purchases"] = self._get_prompt_purchases(prompt_id)
|
143 |
+
|
144 |
+
return {
|
145 |
+
"success": True,
|
146 |
+
"prompt": prompt_dict
|
147 |
+
}
|
148 |
+
|
149 |
+
def _get_prompt_purchases(self, prompt_id: str) -> Dict[str, Any]:
|
150 |
+
"""Get purchase information for a prompt"""
|
151 |
+
try:
|
152 |
+
with open(self.sales_file, "r") as f:
|
153 |
+
sales = json.load(f)
|
154 |
+
|
155 |
+
# Filter sales for this prompt
|
156 |
+
prompt_sales = [s for s in sales if s.get("prompt_id") == prompt_id]
|
157 |
+
|
158 |
+
return {
|
159 |
+
"total": len(prompt_sales),
|
160 |
+
"recent": len([s for s in prompt_sales if
|
161 |
+
(datetime.now() - datetime.fromisoformat(s.get("timestamp", ""))).days < 7])
|
162 |
+
}
|
163 |
+
except Exception as e:
|
164 |
+
logger.error(f"Error getting prompt purchases: {e}")
|
165 |
+
return {"total": 0, "recent": 0}
|
166 |
+
|
167 |
+
def create_prompt_for_sale(self,
|
168 |
+
name: str,
|
169 |
+
description: str,
|
170 |
+
template: str,
|
171 |
+
system_message: str,
|
172 |
+
category: str,
|
173 |
+
price: float,
|
174 |
+
parameters: Dict[str, Any],
|
175 |
+
created_by: str,
|
176 |
+
provider_defaults: Dict[str, Any] = None) -> Dict[str, Any]:
|
177 |
+
"""
|
178 |
+
Create a new prompt template for sale in the marketplace
|
179 |
+
|
180 |
+
Args:
|
181 |
+
name: Name of the prompt template
|
182 |
+
description: Description of what the prompt does
|
183 |
+
template: The prompt template text
|
184 |
+
system_message: System message for the prompt
|
185 |
+
category: Category for the prompt
|
186 |
+
price: Price in credits
|
187 |
+
parameters: Dictionary of parameters for the prompt
|
188 |
+
created_by: User ID of the creator
|
189 |
+
provider_defaults: Default settings for different providers
|
190 |
+
|
191 |
+
Returns:
|
192 |
+
Result dictionary with success status and prompt information
|
193 |
+
"""
|
194 |
+
# Validate inputs
|
195 |
+
if not name or not template:
|
196 |
+
return {"success": False, "error": "Name and template are required"}
|
197 |
+
|
198 |
+
if price < 0:
|
199 |
+
return {"success": False, "error": "Price cannot be negative"}
|
200 |
+
|
201 |
+
# Create the prompt template
|
202 |
+
new_prompt = PromptTemplate(
|
203 |
+
name=name,
|
204 |
+
description=description,
|
205 |
+
template=template,
|
206 |
+
system_message=system_message,
|
207 |
+
category=category,
|
208 |
+
is_public=True, # It's for sale, so make it public
|
209 |
+
created_by=created_by,
|
210 |
+
price=price,
|
211 |
+
parameters=parameters,
|
212 |
+
provider_defaults=provider_defaults or {}
|
213 |
+
)
|
214 |
+
|
215 |
+
# Save the prompt
|
216 |
+
created_prompt = self.template_manager.create_template(new_prompt)
|
217 |
+
|
218 |
+
# Initialize stats for this prompt
|
219 |
+
self._initialize_prompt_stats(created_prompt.id)
|
220 |
+
|
221 |
+
return {
|
222 |
+
"success": True,
|
223 |
+
"prompt": created_prompt.to_dict(),
|
224 |
+
"message": "Prompt successfully created and listed in the marketplace"
|
225 |
+
}
|
226 |
+
|
227 |
+
def _initialize_prompt_stats(self, prompt_id: str):
|
228 |
+
"""Initialize statistics for a new prompt"""
|
229 |
+
try:
|
230 |
+
with open(self.stats_file, "r") as f:
|
231 |
+
stats = json.load(f)
|
232 |
+
|
233 |
+
prompt_usage = stats.get("prompt_usage", {})
|
234 |
+
prompt_usage[prompt_id] = {
|
235 |
+
"count": 0,
|
236 |
+
"purchases": 0,
|
237 |
+
"avg_rating": 0,
|
238 |
+
"review_count": 0,
|
239 |
+
"last_used": None
|
240 |
+
}
|
241 |
+
|
242 |
+
stats["prompt_usage"] = prompt_usage
|
243 |
+
|
244 |
+
with open(self.stats_file, "w") as f:
|
245 |
+
json.dump(stats, f, indent=2)
|
246 |
+
|
247 |
+
except Exception as e:
|
248 |
+
logger.error(f"Error initializing prompt stats: {e}")
|
249 |
+
|
250 |
+
def purchase_prompt(self, prompt_id: str, user_id: str, credits_available: float) -> Dict[str, Any]:
|
251 |
+
"""
|
252 |
+
Purchase a prompt from the marketplace
|
253 |
+
|
254 |
+
Args:
|
255 |
+
prompt_id: ID of the prompt to purchase
|
256 |
+
user_id: ID of the user making the purchase
|
257 |
+
credits_available: Credits available to the user
|
258 |
+
|
259 |
+
Returns:
|
260 |
+
Result dictionary with success status and transaction details
|
261 |
+
"""
|
262 |
+
# Get the prompt
|
263 |
+
prompt = self.template_manager.get_template(prompt_id)
|
264 |
+
|
265 |
+
if not prompt:
|
266 |
+
return {"success": False, "error": "Prompt not found"}
|
267 |
+
|
268 |
+
if not prompt.is_public:
|
269 |
+
return {"success": False, "error": "Prompt is not available for purchase"}
|
270 |
+
|
271 |
+
# Check if user already owns this prompt
|
272 |
+
if self._user_owns_prompt(user_id, prompt_id):
|
273 |
+
return {"success": False, "error": "You already own this prompt"}
|
274 |
+
|
275 |
+
# Check if user has enough credits
|
276 |
+
if credits_available < prompt.price:
|
277 |
+
return {"success": False, "error": "Insufficient credits", "credits_needed": prompt.price}
|
278 |
+
|
279 |
+
# Process the purchase
|
280 |
+
purchase_id = str(uuid.uuid4())
|
281 |
+
purchase_time = datetime.now().isoformat()
|
282 |
+
|
283 |
+
purchase_record = {
|
284 |
+
"id": purchase_id,
|
285 |
+
"prompt_id": prompt_id,
|
286 |
+
"user_id": user_id,
|
287 |
+
"seller_id": prompt.created_by,
|
288 |
+
"price": prompt.price,
|
289 |
+
"timestamp": purchase_time
|
290 |
+
}
|
291 |
+
|
292 |
+
# Record the purchase
|
293 |
+
self._record_purchase(purchase_record)
|
294 |
+
|
295 |
+
# Record the sale
|
296 |
+
self._record_sale(purchase_record)
|
297 |
+
|
298 |
+
# Update statistics
|
299 |
+
self._update_stats_after_purchase(prompt_id)
|
300 |
+
|
301 |
+
return {
|
302 |
+
"success": True,
|
303 |
+
"transaction": {
|
304 |
+
"id": purchase_id,
|
305 |
+
"prompt_id": prompt_id,
|
306 |
+
"prompt_name": prompt.name,
|
307 |
+
"price": prompt.price,
|
308 |
+
"timestamp": purchase_time
|
309 |
+
},
|
310 |
+
"credits_used": prompt.price,
|
311 |
+
"message": f"Successfully purchased prompt: {prompt.name}"
|
312 |
+
}
|
313 |
+
|
314 |
+
def _user_owns_prompt(self, user_id: str, prompt_id: str) -> bool:
|
315 |
+
"""Check if a user already owns a prompt"""
|
316 |
+
try:
|
317 |
+
with open(self.purchases_file, "r") as f:
|
318 |
+
purchases = json.load(f)
|
319 |
+
|
320 |
+
# Check if the user has already purchased this prompt
|
321 |
+
for purchase in purchases:
|
322 |
+
if purchase.get("user_id") == user_id and purchase.get("prompt_id") == prompt_id:
|
323 |
+
return True
|
324 |
+
|
325 |
+
return False
|
326 |
+
|
327 |
+
except Exception as e:
|
328 |
+
logger.error(f"Error checking if user owns prompt: {e}")
|
329 |
+
return False
|
330 |
+
|
331 |
+
def _record_purchase(self, purchase_record: Dict[str, Any]):
|
332 |
+
"""Record a prompt purchase"""
|
333 |
+
try:
|
334 |
+
with open(self.purchases_file, "r") as f:
|
335 |
+
purchases = json.load(f)
|
336 |
+
|
337 |
+
purchases.append(purchase_record)
|
338 |
+
|
339 |
+
with open(self.purchases_file, "w") as f:
|
340 |
+
json.dump(purchases, f, indent=2)
|
341 |
+
|
342 |
+
except Exception as e:
|
343 |
+
logger.error(f"Error recording purchase: {e}")
|
344 |
+
|
345 |
+
def _record_sale(self, purchase_record: Dict[str, Any]):
|
346 |
+
"""Record a prompt sale"""
|
347 |
+
try:
|
348 |
+
with open(self.sales_file, "r") as f:
|
349 |
+
sales = json.load(f)
|
350 |
+
|
351 |
+
sales.append(purchase_record)
|
352 |
+
|
353 |
+
with open(self.sales_file, "w") as f:
|
354 |
+
json.dump(sales, f, indent=2)
|
355 |
+
|
356 |
+
except Exception as e:
|
357 |
+
logger.error(f"Error recording sale: {e}")
|
358 |
+
|
359 |
+
def _update_stats_after_purchase(self, prompt_id: str):
|
360 |
+
"""Update statistics after a prompt purchase"""
|
361 |
+
try:
|
362 |
+
with open(self.stats_file, "r") as f:
|
363 |
+
stats = json.load(f)
|
364 |
+
|
365 |
+
# Update prompt-specific stats
|
366 |
+
prompt_usage = stats.get("prompt_usage", {})
|
367 |
+
if prompt_id not in prompt_usage:
|
368 |
+
prompt_usage[prompt_id] = {
|
369 |
+
"count": 0,
|
370 |
+
"purchases": 0,
|
371 |
+
"avg_rating": 0,
|
372 |
+
"review_count": 0
|
373 |
+
}
|
374 |
+
|
375 |
+
prompt_usage[prompt_id]["purchases"] = prompt_usage[prompt_id].get("purchases", 0) + 1
|
376 |
+
|
377 |
+
# Update global stats
|
378 |
+
stats["total_sales"] = stats.get("total_sales", 0) + 1
|
379 |
+
|
380 |
+
# Get the prompt price
|
381 |
+
prompt = self.template_manager.get_template(prompt_id)
|
382 |
+
if prompt:
|
383 |
+
stats["total_revenue"] = stats.get("total_revenue", 0) + prompt.price
|
384 |
+
|
385 |
+
# Update category popularity
|
386 |
+
category = prompt.category
|
387 |
+
popular_categories = stats.get("popular_categories", {})
|
388 |
+
popular_categories[category] = popular_categories.get(category, 0) + 1
|
389 |
+
stats["popular_categories"] = popular_categories
|
390 |
+
|
391 |
+
# Save updated stats
|
392 |
+
with open(self.stats_file, "w") as f:
|
393 |
+
json.dump(stats, f, indent=2)
|
394 |
+
|
395 |
+
except Exception as e:
|
396 |
+
logger.error(f"Error updating stats after purchase: {e}")
|
397 |
+
|
398 |
+
def get_user_purchased_prompts(self, user_id: str) -> List[Dict[str, Any]]:
|
399 |
+
"""
|
400 |
+
Get prompts purchased by a specific user
|
401 |
+
|
402 |
+
Args:
|
403 |
+
user_id: ID of the user
|
404 |
+
|
405 |
+
Returns:
|
406 |
+
List of prompts owned by the user
|
407 |
+
"""
|
408 |
+
purchased_prompt_ids = self._get_user_purchased_prompt_ids(user_id)
|
409 |
+
|
410 |
+
# Get the prompt details for each purchased prompt
|
411 |
+
purchased_prompts = []
|
412 |
+
for prompt_id in purchased_prompt_ids:
|
413 |
+
prompt = self.template_manager.get_template(prompt_id)
|
414 |
+
if prompt:
|
415 |
+
prompt_dict = prompt.to_dict()
|
416 |
+
purchased_prompts.append(prompt_dict)
|
417 |
+
|
418 |
+
return purchased_prompts
|
419 |
+
|
420 |
+
def _get_user_purchased_prompt_ids(self, user_id: str) -> List[str]:
|
421 |
+
"""Get IDs of prompts purchased by a user"""
|
422 |
+
try:
|
423 |
+
with open(self.purchases_file, "r") as f:
|
424 |
+
purchases = json.load(f)
|
425 |
+
|
426 |
+
# Get unique prompt IDs purchased by this user
|
427 |
+
prompt_ids = set()
|
428 |
+
for purchase in purchases:
|
429 |
+
if purchase.get("user_id") == user_id:
|
430 |
+
prompt_ids.add(purchase.get("prompt_id"))
|
431 |
+
|
432 |
+
return list(prompt_ids)
|
433 |
+
|
434 |
+
except Exception as e:
|
435 |
+
logger.error(f"Error getting user purchased prompt IDs: {e}")
|
436 |
+
return []
|
437 |
+
|
438 |
+
def get_user_sales(self, user_id: str) -> Dict[str, Any]:
|
439 |
+
"""
|
440 |
+
Get sales information for a specific seller
|
441 |
+
|
442 |
+
Args:
|
443 |
+
user_id: ID of the seller
|
444 |
+
|
445 |
+
Returns:
|
446 |
+
Dictionary with sales information
|
447 |
+
"""
|
448 |
+
try:
|
449 |
+
with open(self.sales_file, "r") as f:
|
450 |
+
sales = json.load(f)
|
451 |
+
|
452 |
+
# Filter sales by this seller
|
453 |
+
user_sales = [s for s in sales if s.get("seller_id") == user_id]
|
454 |
+
|
455 |
+
# Calculate total revenue
|
456 |
+
total_revenue = sum(s.get("price", 0) for s in user_sales)
|
457 |
+
|
458 |
+
# Group sales by prompt
|
459 |
+
sales_by_prompt = {}
|
460 |
+
for sale in user_sales:
|
461 |
+
prompt_id = sale.get("prompt_id")
|
462 |
+
if prompt_id not in sales_by_prompt:
|
463 |
+
sales_by_prompt[prompt_id] = []
|
464 |
+
sales_by_prompt[prompt_id].append(sale)
|
465 |
+
|
466 |
+
# Get prompt details and calculate stats for each prompt
|
467 |
+
prompt_sales = []
|
468 |
+
for prompt_id, sales_list in sales_by_prompt.items():
|
469 |
+
prompt = self.template_manager.get_template(prompt_id)
|
470 |
+
if not prompt:
|
471 |
+
continue
|
472 |
+
|
473 |
+
prompt_revenue = sum(s.get("price", 0) for s in sales_list)
|
474 |
+
|
475 |
+
prompt_sales.append({
|
476 |
+
"prompt_id": prompt_id,
|
477 |
+
"prompt_name": prompt.name,
|
478 |
+
"price": prompt.price,
|
479 |
+
"sales_count": len(sales_list),
|
480 |
+
"revenue": prompt_revenue,
|
481 |
+
"last_sale": max(s.get("timestamp", "") for s in sales_list)
|
482 |
+
})
|
483 |
+
|
484 |
+
# Sort by revenue
|
485 |
+
prompt_sales.sort(key=lambda x: x.get("revenue", 0), reverse=True)
|
486 |
+
|
487 |
+
return {
|
488 |
+
"success": True,
|
489 |
+
"total_sales": len(user_sales),
|
490 |
+
"total_revenue": total_revenue,
|
491 |
+
"prompt_count": len(prompt_sales),
|
492 |
+
"prompts": prompt_sales
|
493 |
+
}
|
494 |
+
|
495 |
+
except Exception as e:
|
496 |
+
logger.error(f"Error getting user sales: {e}")
|
497 |
+
return {
|
498 |
+
"success": False,
|
499 |
+
"error": str(e)
|
500 |
+
}
|
501 |
+
|
502 |
+
def record_prompt_usage(self, prompt_id: str, user_id: str, provider: str = None) -> bool:
|
503 |
+
"""
|
504 |
+
Record usage of a prompt
|
505 |
+
|
506 |
+
Args:
|
507 |
+
prompt_id: ID of the prompt used
|
508 |
+
user_id: ID of the user using the prompt
|
509 |
+
provider: Optional provider used
|
510 |
+
|
511 |
+
Returns:
|
512 |
+
Success status
|
513 |
+
"""
|
514 |
+
try:
|
515 |
+
with open(self.stats_file, "r") as f:
|
516 |
+
stats = json.load(f)
|
517 |
+
|
518 |
+
# Update prompt usage stats
|
519 |
+
prompt_usage = stats.get("prompt_usage", {})
|
520 |
+
if prompt_id not in prompt_usage:
|
521 |
+
prompt_usage[prompt_id] = {
|
522 |
+
"count": 0,
|
523 |
+
"purchases": 0,
|
524 |
+
"avg_rating": 0,
|
525 |
+
"review_count": 0
|
526 |
+
}
|
527 |
+
|
528 |
+
prompt_usage[prompt_id]["count"] = prompt_usage[prompt_id].get("count", 0) + 1
|
529 |
+
prompt_usage[prompt_id]["last_used"] = datetime.now().isoformat()
|
530 |
+
|
531 |
+
# Track provider usage if provided
|
532 |
+
if provider:
|
533 |
+
providers = prompt_usage[prompt_id].get("providers", {})
|
534 |
+
providers[provider] = providers.get(provider, 0) + 1
|
535 |
+
prompt_usage[prompt_id]["providers"] = providers
|
536 |
+
|
537 |
+
# Save updated stats
|
538 |
+
with open(self.stats_file, "w") as f:
|
539 |
+
json.dump(stats, f, indent=2)
|
540 |
+
|
541 |
+
return True
|
542 |
+
|
543 |
+
except Exception as e:
|
544 |
+
logger.error(f"Error recording prompt usage: {e}")
|
545 |
+
return False
|
546 |
+
|
547 |
+
def rate_prompt(self, prompt_id: str, user_id: str, rating: int, review: str = None) -> Dict[str, Any]:
|
548 |
+
"""
|
549 |
+
Rate and review a prompt
|
550 |
+
|
551 |
+
Args:
|
552 |
+
prompt_id: ID of the prompt to rate
|
553 |
+
user_id: ID of the user providing the rating
|
554 |
+
rating: Rating value (1-5)
|
555 |
+
review: Optional review text
|
556 |
+
|
557 |
+
Returns:
|
558 |
+
Result dictionary with success status
|
559 |
+
"""
|
560 |
+
# Validate rating
|
561 |
+
if rating < 1 or rating > 5:
|
562 |
+
return {"success": False, "error": "Rating must be between 1 and 5"}
|
563 |
+
|
564 |
+
# Check if the user owns the prompt
|
565 |
+
if not self._user_owns_prompt(user_id, prompt_id):
|
566 |
+
return {"success": False, "error": "You must purchase a prompt before rating it"}
|
567 |
+
|
568 |
+
try:
|
569 |
+
# Update the stats with the new rating
|
570 |
+
with open(self.stats_file, "r") as f:
|
571 |
+
stats = json.load(f)
|
572 |
+
|
573 |
+
prompt_usage = stats.get("prompt_usage", {})
|
574 |
+
if prompt_id not in prompt_usage:
|
575 |
+
return {"success": False, "error": "Prompt not found"}
|
576 |
+
|
577 |
+
# Calculate new average rating
|
578 |
+
current_avg = prompt_usage[prompt_id].get("avg_rating", 0)
|
579 |
+
current_count = prompt_usage[prompt_id].get("review_count", 0)
|
580 |
+
|
581 |
+
if current_count == 0:
|
582 |
+
new_avg = rating
|
583 |
+
else:
|
584 |
+
new_avg = (current_avg * current_count + rating) / (current_count + 1)
|
585 |
+
|
586 |
+
prompt_usage[prompt_id]["avg_rating"] = new_avg
|
587 |
+
prompt_usage[prompt_id]["review_count"] = current_count + 1
|
588 |
+
|
589 |
+
# Save the review if provided
|
590 |
+
if review:
|
591 |
+
reviews = prompt_usage[prompt_id].get("reviews", [])
|
592 |
+
reviews.append({
|
593 |
+
"user_id": user_id,
|
594 |
+
"rating": rating,
|
595 |
+
"review": review,
|
596 |
+
"timestamp": datetime.now().isoformat()
|
597 |
+
})
|
598 |
+
prompt_usage[prompt_id]["reviews"] = reviews
|
599 |
+
|
600 |
+
# Save updated stats
|
601 |
+
with open(self.stats_file, "w") as f:
|
602 |
+
json.dump(stats, f, indent=2)
|
603 |
+
|
604 |
+
return {
|
605 |
+
"success": True,
|
606 |
+
"message": "Rating submitted successfully",
|
607 |
+
"new_rating": new_avg,
|
608 |
+
"review_count": current_count + 1
|
609 |
+
}
|
610 |
+
|
611 |
+
except Exception as e:
|
612 |
+
logger.error(f"Error rating prompt: {e}")
|
613 |
+
return {
|
614 |
+
"success": False,
|
615 |
+
"error": str(e)
|
616 |
+
}
|
617 |
+
|
618 |
+
def get_marketplace_stats(self) -> Dict[str, Any]:
|
619 |
+
"""Get overall marketplace statistics"""
|
620 |
+
try:
|
621 |
+
with open(self.stats_file, "r") as f:
|
622 |
+
stats = json.load(f)
|
623 |
+
|
624 |
+
# Get the top prompts by usage
|
625 |
+
prompt_usage = stats.get("prompt_usage", {})
|
626 |
+
top_prompts = []
|
627 |
+
|
628 |
+
for prompt_id, usage in prompt_usage.items():
|
629 |
+
prompt = self.template_manager.get_template(prompt_id)
|
630 |
+
if not prompt:
|
631 |
+
continue
|
632 |
+
|
633 |
+
top_prompts.append({
|
634 |
+
"id": prompt_id,
|
635 |
+
"name": prompt.name,
|
636 |
+
"creator": prompt.created_by,
|
637 |
+
"category": prompt.category,
|
638 |
+
"price": prompt.price,
|
639 |
+
"usage_count": usage.get("count", 0),
|
640 |
+
"purchase_count": usage.get("purchases", 0),
|
641 |
+
"rating": usage.get("avg_rating", 0),
|
642 |
+
"review_count": usage.get("review_count", 0)
|
643 |
+
})
|
644 |
+
|
645 |
+
# Sort by usage count
|
646 |
+
top_prompts.sort(key=lambda x: x.get("usage_count", 0), reverse=True)
|
647 |
+
top_prompts = top_prompts[:10] # Get top 10
|
648 |
+
|
649 |
+
# Get top categories
|
650 |
+
categories = stats.get("popular_categories", {})
|
651 |
+
top_categories = [{"category": k, "count": v} for k, v in categories.items()]
|
652 |
+
top_categories.sort(key=lambda x: x.get("count", 0), reverse=True)
|
653 |
+
|
654 |
+
return {
|
655 |
+
"success": True,
|
656 |
+
"total_sales": stats.get("total_sales", 0),
|
657 |
+
"total_revenue": stats.get("total_revenue", 0),
|
658 |
+
"top_prompts": top_prompts,
|
659 |
+
"top_categories": top_categories
|
660 |
+
}
|
661 |
+
|
662 |
+
except Exception as e:
|
663 |
+
logger.error(f"Error getting marketplace stats: {e}")
|
664 |
+
return {
|
665 |
+
"success": False,
|
666 |
+
"error": str(e)
|
667 |
+
}
|
668 |
+
|
669 |
+
# Example usage
|
670 |
+
if __name__ == "__main__":
|
671 |
+
# Initialize the marketplace
|
672 |
+
marketplace = PromptMarketplace()
|
673 |
+
|
674 |
+
# Create a sample prompt for sale
|
675 |
+
prompt_result = marketplace.create_prompt_for_sale(
|
676 |
+
name="Advanced SEO Article Writer",
|
677 |
+
description="Generate comprehensive SEO-optimized articles with proper keyword placement and structure",
|
678 |
+
template="Write a {length} word SEO-optimized article about {topic}. Target the keyword {keyword} with a keyword density of {density}%. Include {headings} headings, a compelling introduction, and a conclusion with call-to-action.",
|
679 |
+
system_message="You are an expert SEO content writer who creates engaging, well-researched content that ranks well in search engines.",
|
680 |
+
category="marketing",
|
681 |
+
price=25.0,
|
682 |
+
parameters={
|
683 |
+
"topic": {"type": "string", "description": "Main topic of the article", "required": True},
|
684 |
+
"keyword": {"type": "string", "description": "Target keyword to optimize for", "required": True},
|
685 |
+
"length": {"type": "number", "description": "Word count", "default": 1500},
|
686 |
+
"density": {"type": "number", "description": "Keyword density percentage", "default": 2},
|
687 |
+
"headings": {"type": "number", "description": "Number of headings to include", "default": 5}
|
688 |
+
},
|
689 |
+
created_by="seller123",
|
690 |
+
provider_defaults={
|
691 |
+
"openai": {"model": "gpt-4-turbo"}
|
692 |
+
}
|
693 |
+
)
|
694 |
+
|
695 |
+
print(f"Created prompt: {prompt_result['success']}")
|
696 |
+
|
697 |
+
if prompt_result['success']:
|
698 |
+
# Simulate a purchase
|
699 |
+
purchase_result = marketplace.purchase_prompt(
|
700 |
+
prompt_id=prompt_result['prompt']['id'],
|
701 |
+
user_id="buyer456",
|
702 |
+
credits_available=100.0
|
703 |
+
)
|
704 |
+
|
705 |
+
print(f"Purchase result: {purchase_result['success']}")
|
706 |
+
|
707 |
+
# Record usage of the prompt
|
708 |
+
marketplace.record_prompt_usage(
|
709 |
+
prompt_id=prompt_result['prompt']['id'],
|
710 |
+
user_id="buyer456",
|
711 |
+
provider="openai"
|
712 |
+
)
|
713 |
+
|
714 |
+
# Rate the prompt
|
715 |
+
rating_result = marketplace.rate_prompt(
|
716 |
+
prompt_id=prompt_result['prompt']['id'],
|
717 |
+
user_id="buyer456",
|
718 |
+
rating=5,
|
719 |
+
review="This prompt generated an excellent SEO article that ranked quickly!"
|
720 |
+
)
|
721 |
+
|
722 |
+
print(f"Rating result: {rating_result['success']}")
|
723 |
+
|
724 |
+
# Get marketplace stats
|
725 |
+
stats = marketplace.get_marketplace_stats()
|
726 |
+
print(f"Marketplace stats: {stats['success']}")
|
727 |
+
if stats['success']:
|
728 |
+
print(f"Total sales: {stats['total_sales']}")
|
prompts/prompt_templates.py
ADDED
@@ -0,0 +1,350 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Prompt Templates Module
|
3 |
+
Manages prompt templates for different AI tools and providers
|
4 |
+
Includes system for storing, loading, and managing user-created prompts
|
5 |
+
"""
|
6 |
+
import os
|
7 |
+
import json
|
8 |
+
import uuid
|
9 |
+
import logging
|
10 |
+
from typing import Dict, Any, List, Optional
|
11 |
+
from datetime import datetime
|
12 |
+
|
13 |
+
# Setup logging
|
14 |
+
logging.basicConfig(level=logging.INFO)
|
15 |
+
logger = logging.getLogger("prompts")
|
16 |
+
|
17 |
+
class PromptTemplate:
|
18 |
+
"""Represents a prompt template that can be used with AI providers"""
|
19 |
+
|
20 |
+
def __init__(self,
|
21 |
+
id: str = None,
|
22 |
+
name: str = "",
|
23 |
+
description: str = "",
|
24 |
+
template: str = "",
|
25 |
+
system_message: str = "",
|
26 |
+
category: str = "",
|
27 |
+
is_public: bool = False,
|
28 |
+
created_by: str = "system",
|
29 |
+
created_at: str = None,
|
30 |
+
price: float = 0.0,
|
31 |
+
parameters: Dict[str, Any] = None,
|
32 |
+
provider_defaults: Dict[str, Any] = None):
|
33 |
+
"""Initialize a prompt template"""
|
34 |
+
self.id = id or str(uuid.uuid4())
|
35 |
+
self.name = name
|
36 |
+
self.description = description
|
37 |
+
self.template = template
|
38 |
+
self.system_message = system_message
|
39 |
+
self.category = category
|
40 |
+
self.is_public = is_public
|
41 |
+
self.created_by = created_by
|
42 |
+
self.created_at = created_at or datetime.now().isoformat()
|
43 |
+
self.price = price
|
44 |
+
self.parameters = parameters or {}
|
45 |
+
self.provider_defaults = provider_defaults or {}
|
46 |
+
|
47 |
+
def to_dict(self) -> Dict[str, Any]:
|
48 |
+
"""Convert template to dictionary"""
|
49 |
+
return {
|
50 |
+
"id": self.id,
|
51 |
+
"name": self.name,
|
52 |
+
"description": self.description,
|
53 |
+
"template": self.template,
|
54 |
+
"system_message": self.system_message,
|
55 |
+
"category": self.category,
|
56 |
+
"is_public": self.is_public,
|
57 |
+
"created_by": self.created_by,
|
58 |
+
"created_at": self.created_at,
|
59 |
+
"price": self.price,
|
60 |
+
"parameters": self.parameters,
|
61 |
+
"provider_defaults": self.provider_defaults
|
62 |
+
}
|
63 |
+
|
64 |
+
@classmethod
|
65 |
+
def from_dict(cls, data: Dict[str, Any]) -> 'PromptTemplate':
|
66 |
+
"""Create template from dictionary"""
|
67 |
+
return cls(
|
68 |
+
id=data.get("id"),
|
69 |
+
name=data.get("name", ""),
|
70 |
+
description=data.get("description", ""),
|
71 |
+
template=data.get("template", ""),
|
72 |
+
system_message=data.get("system_message", ""),
|
73 |
+
category=data.get("category", ""),
|
74 |
+
is_public=data.get("is_public", False),
|
75 |
+
created_by=data.get("created_by", "system"),
|
76 |
+
created_at=data.get("created_at"),
|
77 |
+
price=data.get("price", 0.0),
|
78 |
+
parameters=data.get("parameters", {}),
|
79 |
+
provider_defaults=data.get("provider_defaults", {})
|
80 |
+
)
|
81 |
+
|
82 |
+
def render(self, variables: Dict[str, Any] = None) -> Dict[str, str]:
|
83 |
+
"""
|
84 |
+
Render the prompt template with the provided variables
|
85 |
+
Returns both the rendered prompt and system message
|
86 |
+
"""
|
87 |
+
variables = variables or {}
|
88 |
+
prompt = self.template
|
89 |
+
system = self.system_message
|
90 |
+
|
91 |
+
# Replace variables in the template
|
92 |
+
for key, value in variables.items():
|
93 |
+
placeholder = "{" + key + "}"
|
94 |
+
prompt = prompt.replace(placeholder, str(value))
|
95 |
+
system = system.replace(placeholder, str(value))
|
96 |
+
|
97 |
+
return {
|
98 |
+
"prompt": prompt,
|
99 |
+
"system_message": system
|
100 |
+
}
|
101 |
+
|
102 |
+
class PromptTemplateManager:
|
103 |
+
"""Manages prompt templates, including default and user-created ones"""
|
104 |
+
|
105 |
+
def __init__(self, templates_dir: str = None):
|
106 |
+
"""Initialize the prompt template manager"""
|
107 |
+
self.templates_dir = templates_dir or os.path.join(os.path.dirname(__file__), "templates")
|
108 |
+
self.user_templates_dir = os.path.join(self.templates_dir, "user")
|
109 |
+
|
110 |
+
# Create directories if they don't exist
|
111 |
+
os.makedirs(self.templates_dir, exist_ok=True)
|
112 |
+
os.makedirs(self.user_templates_dir, exist_ok=True)
|
113 |
+
|
114 |
+
# Load default templates
|
115 |
+
self.default_templates = self._load_default_templates()
|
116 |
+
|
117 |
+
# Load user templates
|
118 |
+
self.user_templates = self._load_user_templates()
|
119 |
+
|
120 |
+
def _load_default_templates(self) -> Dict[str, PromptTemplate]:
|
121 |
+
"""Load default templates from files"""
|
122 |
+
templates = {}
|
123 |
+
|
124 |
+
# Define default templates if no files exist yet
|
125 |
+
default_templates = {
|
126 |
+
"general_chat": PromptTemplate(
|
127 |
+
id="general_chat",
|
128 |
+
name="General Chat",
|
129 |
+
description="A general-purpose chat prompt",
|
130 |
+
template="Please answer the following question or respond to the message: {input}",
|
131 |
+
system_message="You are a helpful assistant that provides accurate and concise responses.",
|
132 |
+
category="general",
|
133 |
+
created_by="system"
|
134 |
+
),
|
135 |
+
"creative_writing": PromptTemplate(
|
136 |
+
id="creative_writing",
|
137 |
+
name="Creative Writing",
|
138 |
+
description="Generate creative writing based on a premise",
|
139 |
+
template="Write a {genre} {format} about {topic}.",
|
140 |
+
system_message="You are a creative writer with expertise in different genres and formats.",
|
141 |
+
category="creative",
|
142 |
+
created_by="system",
|
143 |
+
parameters={
|
144 |
+
"genre": {"type": "string", "description": "Genre of the writing", "default": "science fiction"},
|
145 |
+
"format": {"type": "string", "description": "Format of the writing", "default": "short story"},
|
146 |
+
"topic": {"type": "string", "description": "Topic or premise", "required": True}
|
147 |
+
}
|
148 |
+
),
|
149 |
+
"code_assistant": PromptTemplate(
|
150 |
+
id="code_assistant",
|
151 |
+
name="Code Assistant",
|
152 |
+
description="Generate or debug code in various languages",
|
153 |
+
template="I need help with the following code task in {language}:\n\n{task}\n\n{code}",
|
154 |
+
system_message="You are an expert programmer. Provide well-commented, efficient, and correct code solutions.",
|
155 |
+
category="development",
|
156 |
+
created_by="system",
|
157 |
+
parameters={
|
158 |
+
"language": {"type": "string", "description": "Programming language", "required": True},
|
159 |
+
"task": {"type": "string", "description": "Description of the coding task", "required": True},
|
160 |
+
"code": {"type": "string", "description": "Existing code (if any)", "default": ""}
|
161 |
+
},
|
162 |
+
provider_defaults={
|
163 |
+
"openai": {"model": "gpt-4-turbo"},
|
164 |
+
"deepseek": {"model": "deepseek-coder"}
|
165 |
+
}
|
166 |
+
),
|
167 |
+
"image_prompt": PromptTemplate(
|
168 |
+
id="image_prompt",
|
169 |
+
name="Image Generation",
|
170 |
+
description="Detailed prompt for image generation",
|
171 |
+
template="{subject} {style}, {details}, {quality}",
|
172 |
+
system_message="",
|
173 |
+
category="images",
|
174 |
+
created_by="system",
|
175 |
+
parameters={
|
176 |
+
"subject": {"type": "string", "description": "Main subject of the image", "required": True},
|
177 |
+
"style": {"type": "string", "description": "Art style", "default": "digital art"},
|
178 |
+
"details": {"type": "string", "description": "Additional details", "default": "detailed, vibrant colors"},
|
179 |
+
"quality": {"type": "string", "description": "Quality descriptors", "default": "high quality, 4k, trending on artstation"}
|
180 |
+
},
|
181 |
+
provider_defaults={
|
182 |
+
"openai": {"model": "dall-e-3"}
|
183 |
+
}
|
184 |
+
)
|
185 |
+
}
|
186 |
+
|
187 |
+
# Save default templates if they don't exist
|
188 |
+
for template_id, template in default_templates.items():
|
189 |
+
template_path = os.path.join(self.templates_dir, f"{template_id}.json")
|
190 |
+
|
191 |
+
if not os.path.exists(template_path):
|
192 |
+
with open(template_path, "w") as f:
|
193 |
+
json.dump(template.to_dict(), f, indent=2)
|
194 |
+
|
195 |
+
templates[template_id] = template
|
196 |
+
|
197 |
+
return templates
|
198 |
+
|
199 |
+
def _load_user_templates(self) -> Dict[str, PromptTemplate]:
|
200 |
+
"""Load user-created templates"""
|
201 |
+
templates = {}
|
202 |
+
|
203 |
+
try:
|
204 |
+
# List all JSON files in user_templates_dir
|
205 |
+
for filename in os.listdir(self.user_templates_dir):
|
206 |
+
if filename.endswith(".json"):
|
207 |
+
template_path = os.path.join(self.user_templates_dir, filename)
|
208 |
+
|
209 |
+
with open(template_path, "r") as f:
|
210 |
+
template_data = json.load(f)
|
211 |
+
template = PromptTemplate.from_dict(template_data)
|
212 |
+
templates[template.id] = template
|
213 |
+
except Exception as e:
|
214 |
+
logger.error(f"Error loading user templates: {e}")
|
215 |
+
|
216 |
+
return templates
|
217 |
+
|
218 |
+
def get_all_templates(self) -> List[PromptTemplate]:
|
219 |
+
"""Get all available templates (default + user)"""
|
220 |
+
all_templates = list(self.default_templates.values())
|
221 |
+
|
222 |
+
# Add public user templates and user's own templates
|
223 |
+
for template in self.user_templates.values():
|
224 |
+
if template.is_public:
|
225 |
+
all_templates.append(template)
|
226 |
+
|
227 |
+
return all_templates
|
228 |
+
|
229 |
+
def get_user_templates(self, user_id: str) -> List[PromptTemplate]:
|
230 |
+
"""Get templates created by a specific user"""
|
231 |
+
return [t for t in self.user_templates.values() if t.created_by == user_id]
|
232 |
+
|
233 |
+
def get_template(self, template_id: str) -> Optional[PromptTemplate]:
|
234 |
+
"""Get a specific template by ID"""
|
235 |
+
if template_id in self.default_templates:
|
236 |
+
return self.default_templates[template_id]
|
237 |
+
|
238 |
+
if template_id in self.user_templates:
|
239 |
+
return self.user_templates[template_id]
|
240 |
+
|
241 |
+
return None
|
242 |
+
|
243 |
+
def create_template(self, template: PromptTemplate) -> PromptTemplate:
|
244 |
+
"""Create a new user template"""
|
245 |
+
# Ensure unique ID
|
246 |
+
if template.id in self.default_templates or template.id in self.user_templates:
|
247 |
+
template.id = str(uuid.uuid4())
|
248 |
+
|
249 |
+
# Save template to file
|
250 |
+
template_path = os.path.join(self.user_templates_dir, f"{template.id}.json")
|
251 |
+
|
252 |
+
with open(template_path, "w") as f:
|
253 |
+
json.dump(template.to_dict(), f, indent=2)
|
254 |
+
|
255 |
+
# Add to user templates
|
256 |
+
self.user_templates[template.id] = template
|
257 |
+
|
258 |
+
return template
|
259 |
+
|
260 |
+
def update_template(self, template: PromptTemplate) -> Optional[PromptTemplate]:
|
261 |
+
"""Update an existing user template"""
|
262 |
+
if template.id not in self.user_templates:
|
263 |
+
logger.error(f"Template {template.id} not found or not a user template")
|
264 |
+
return None
|
265 |
+
|
266 |
+
# Save updated template
|
267 |
+
template_path = os.path.join(self.user_templates_dir, f"{template.id}.json")
|
268 |
+
|
269 |
+
with open(template_path, "w") as f:
|
270 |
+
json.dump(template.to_dict(), f, indent=2)
|
271 |
+
|
272 |
+
# Update in memory
|
273 |
+
self.user_templates[template.id] = template
|
274 |
+
|
275 |
+
return template
|
276 |
+
|
277 |
+
def delete_template(self, template_id: str, user_id: str) -> bool:
|
278 |
+
"""Delete a user template"""
|
279 |
+
if template_id not in self.user_templates:
|
280 |
+
logger.error(f"Template {template_id} not found or not a user template")
|
281 |
+
return False
|
282 |
+
|
283 |
+
# Check ownership
|
284 |
+
template = self.user_templates[template_id]
|
285 |
+
if template.created_by != user_id and user_id != "admin":
|
286 |
+
logger.error(f"User {user_id} does not own template {template_id}")
|
287 |
+
return False
|
288 |
+
|
289 |
+
# Delete template file
|
290 |
+
template_path = os.path.join(self.user_templates_dir, f"{template_id}.json")
|
291 |
+
|
292 |
+
try:
|
293 |
+
os.remove(template_path)
|
294 |
+
del self.user_templates[template_id]
|
295 |
+
return True
|
296 |
+
except Exception as e:
|
297 |
+
logger.error(f"Error deleting template {template_id}: {e}")
|
298 |
+
return False
|
299 |
+
|
300 |
+
def get_templates_by_category(self, category: str) -> List[PromptTemplate]:
|
301 |
+
"""Get templates by category"""
|
302 |
+
all_templates = self.get_all_templates()
|
303 |
+
return [t for t in all_templates if t.category.lower() == category.lower()]
|
304 |
+
|
305 |
+
def get_public_templates(self) -> List[PromptTemplate]:
|
306 |
+
"""Get all public templates created by users"""
|
307 |
+
return [t for t in self.user_templates.values() if t.is_public]
|
308 |
+
|
309 |
+
# Example usage
|
310 |
+
if __name__ == "__main__":
|
311 |
+
# Initialize manager
|
312 |
+
manager = PromptTemplateManager()
|
313 |
+
|
314 |
+
# Get all templates
|
315 |
+
all_templates = manager.get_all_templates()
|
316 |
+
print(f"Loaded {len(all_templates)} templates")
|
317 |
+
|
318 |
+
# Create a new template
|
319 |
+
new_template = PromptTemplate(
|
320 |
+
name="SEO Content",
|
321 |
+
description="Generate SEO-optimized content for websites",
|
322 |
+
template="Write SEO-optimized content about {topic} targeting the keyword {keyword}. {tone} tone, {length} words.",
|
323 |
+
system_message="You are an expert SEO content writer who creates engaging, well-researched content that ranks well in search engines.",
|
324 |
+
category="marketing",
|
325 |
+
created_by="user123",
|
326 |
+
is_public=True,
|
327 |
+
parameters={
|
328 |
+
"topic": {"type": "string", "description": "Main topic", "required": True},
|
329 |
+
"keyword": {"type": "string", "description": "Target keyword", "required": True},
|
330 |
+
"tone": {"type": "string", "description": "Content tone", "default": "professional"},
|
331 |
+
"length": {"type": "number", "description": "Content length in words", "default": 500}
|
332 |
+
}
|
333 |
+
)
|
334 |
+
|
335 |
+
created = manager.create_template(new_template)
|
336 |
+
print(f"Created new template: {created.id} - {created.name}")
|
337 |
+
|
338 |
+
# Test rendering a template
|
339 |
+
code_template = manager.get_template("code_assistant")
|
340 |
+
if code_template:
|
341 |
+
rendered = code_template.render({
|
342 |
+
"language": "Python",
|
343 |
+
"task": "Create a function to calculate Fibonacci numbers",
|
344 |
+
"code": "def fibonacci(n):\n # TODO: Implement"
|
345 |
+
})
|
346 |
+
|
347 |
+
print("\nRendered prompt:")
|
348 |
+
print(rendered["prompt"])
|
349 |
+
print("\nSystem message:")
|
350 |
+
|
prompts/templates/code_assistant.json
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"id": "code_assistant",
|
3 |
+
"name": "Code Assistant",
|
4 |
+
"description": "Generate or debug code in various languages",
|
5 |
+
"template": "I need help with the following code task in {language}:\n\n{task}\n\n{code}",
|
6 |
+
"system_message": "You are an expert programmer. Provide well-commented, efficient, and correct code solutions.",
|
7 |
+
"category": "development",
|
8 |
+
"is_public": false,
|
9 |
+
"created_by": "system",
|
10 |
+
"created_at": "2025-03-18T14:56:55.479511",
|
11 |
+
"price": 0.0,
|
12 |
+
"parameters": {
|
13 |
+
"language": {
|
14 |
+
"type": "string",
|
15 |
+
"description": "Programming language",
|
16 |
+
"required": true
|
17 |
+
},
|
18 |
+
"task": {
|
19 |
+
"type": "string",
|
20 |
+
"description": "Description of the coding task",
|
21 |
+
"required": true
|
22 |
+
},
|
23 |
+
"code": {
|
24 |
+
"type": "string",
|
25 |
+
"description": "Existing code (if any)",
|
26 |
+
"default": ""
|
27 |
+
}
|
28 |
+
},
|
29 |
+
"provider_defaults": {
|
30 |
+
"openai": {
|
31 |
+
"model": "gpt-4-turbo"
|
32 |
+
},
|
33 |
+
"deepseek": {
|
34 |
+
"model": "deepseek-coder"
|
35 |
+
}
|
36 |
+
}
|
37 |
+
}
|
prompts/templates/creative_writing.json
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"id": "creative_writing",
|
3 |
+
"name": "Creative Writing",
|
4 |
+
"description": "Generate creative writing based on a premise",
|
5 |
+
"template": "Write a {genre} {format} about {topic}.",
|
6 |
+
"system_message": "You are a creative writer with expertise in different genres and formats.",
|
7 |
+
"category": "creative",
|
8 |
+
"is_public": false,
|
9 |
+
"created_by": "system",
|
10 |
+
"created_at": "2025-03-18T14:56:55.479511",
|
11 |
+
"price": 0.0,
|
12 |
+
"parameters": {
|
13 |
+
"genre": {
|
14 |
+
"type": "string",
|
15 |
+
"description": "Genre of the writing",
|
16 |
+
"default": "science fiction"
|
17 |
+
},
|
18 |
+
"format": {
|
19 |
+
"type": "string",
|
20 |
+
"description": "Format of the writing",
|
21 |
+
"default": "short story"
|
22 |
+
},
|
23 |
+
"topic": {
|
24 |
+
"type": "string",
|
25 |
+
"description": "Topic or premise",
|
26 |
+
"required": true
|
27 |
+
}
|
28 |
+
},
|
29 |
+
"provider_defaults": {}
|
30 |
+
}
|
prompts/templates/general_chat.json
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"id": "general_chat",
|
3 |
+
"name": "General Chat",
|
4 |
+
"description": "A general-purpose chat prompt",
|
5 |
+
"template": "Please answer the following question or respond to the message: {input}",
|
6 |
+
"system_message": "You are a helpful assistant that provides accurate and concise responses.",
|
7 |
+
"category": "general",
|
8 |
+
"is_public": false,
|
9 |
+
"created_by": "system",
|
10 |
+
"created_at": "2025-03-18T14:56:55.479511",
|
11 |
+
"price": 0.0,
|
12 |
+
"parameters": {},
|
13 |
+
"provider_defaults": {}
|
14 |
+
}
|
prompts/templates/image_prompt.json
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"id": "image_prompt",
|
3 |
+
"name": "Image Generation",
|
4 |
+
"description": "Detailed prompt for image generation",
|
5 |
+
"template": "{subject} {style}, {details}, {quality}",
|
6 |
+
"system_message": "",
|
7 |
+
"category": "images",
|
8 |
+
"is_public": false,
|
9 |
+
"created_by": "system",
|
10 |
+
"created_at": "2025-03-18T14:56:55.479511",
|
11 |
+
"price": 0.0,
|
12 |
+
"parameters": {
|
13 |
+
"subject": {
|
14 |
+
"type": "string",
|
15 |
+
"description": "Main subject of the image",
|
16 |
+
"required": true
|
17 |
+
},
|
18 |
+
"style": {
|
19 |
+
"type": "string",
|
20 |
+
"description": "Art style",
|
21 |
+
"default": "digital art"
|
22 |
+
},
|
23 |
+
"details": {
|
24 |
+
"type": "string",
|
25 |
+
"description": "Additional details",
|
26 |
+
"default": "detailed, vibrant colors"
|
27 |
+
},
|
28 |
+
"quality": {
|
29 |
+
"type": "string",
|
30 |
+
"description": "Quality descriptors",
|
31 |
+
"default": "high quality, 4k, trending on artstation"
|
32 |
+
}
|
33 |
+
},
|
34 |
+
"provider_defaults": {
|
35 |
+
"openai": {
|
36 |
+
"model": "dall-e-3"
|
37 |
+
}
|
38 |
+
}
|
39 |
+
}
|
providers/__init__.py
ADDED
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
AI Providers Package
|
3 |
+
Exports provider classes for different AI model providers
|
4 |
+
"""
|
5 |
+
|
6 |
+
from providers.huggingface import HuggingFaceProvider
|
7 |
+
from providers.openai import OpenAIProvider
|
8 |
+
from providers.deepseek import DeepSeekProvider
|
9 |
+
from providers.openrouter import OpenRouterProvider
|
10 |
+
|
11 |
+
__all__ = [
|
12 |
+
'HuggingFaceProvider',
|
13 |
+
'OpenAIProvider',
|
14 |
+
'DeepSeekProvider',
|
15 |
+
'OpenRouterProvider'
|
16 |
+
]
|
17 |
+
|
18 |
+
# Provider registry for easy access
|
19 |
+
PROVIDERS = {
|
20 |
+
'huggingface': HuggingFaceProvider,
|
21 |
+
'openai': OpenAIProvider,
|
22 |
+
'deepseek': DeepSeekProvider,
|
23 |
+
'openrouter': OpenRouterProvider
|
24 |
+
}
|
25 |
+
|
26 |
+
def get_provider(provider_name: str, api_key: str = None):
|
27 |
+
"""
|
28 |
+
Get a provider instance by name
|
29 |
+
|
30 |
+
Args:
|
31 |
+
provider_name: Name of the provider ('huggingface', 'openai', etc.)
|
32 |
+
api_key: Optional API key to use
|
33 |
+
|
34 |
+
Returns:
|
35 |
+
Provider instance or None if provider not found
|
36 |
+
"""
|
37 |
+
provider_class = PROVIDERS.get(provider_name.lower())
|
38 |
+
if not provider_class:
|
39 |
+
return None
|
40 |
+
|
41 |
+
return provider_class(api_key=api_key)
|
providers/__pycache__/__init__.cpython-310.pyc
ADDED
Binary file (1.07 kB). View file
|
|
providers/__pycache__/deepseek.cpython-310.pyc
ADDED
Binary file (4.39 kB). View file
|
|
providers/__pycache__/huggingface.cpython-310.pyc
ADDED
Binary file (4.6 kB). View file
|
|
providers/__pycache__/openai.cpython-310.pyc
ADDED
Binary file (4.63 kB). View file
|
|
providers/__pycache__/openrouter.cpython-310.pyc
ADDED
Binary file (4.48 kB). View file
|
|
providers/deepseek.py
ADDED
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
DeepSeek Provider Integration
|
3 |
+
Handles API calls to DeepSeek for AI model inference
|
4 |
+
"""
|
5 |
+
import os
|
6 |
+
import requests
|
7 |
+
import time
|
8 |
+
import json
|
9 |
+
import logging
|
10 |
+
from typing import Dict, Any, Optional, List
|
11 |
+
|
12 |
+
# Setup logging
|
13 |
+
logging.basicConfig(level=logging.INFO)
|
14 |
+
logger = logging.getLogger("deepseek")
|
15 |
+
|
16 |
+
class DeepSeekProvider:
|
17 |
+
"""DeepSeek API provider for model inference"""
|
18 |
+
|
19 |
+
def __init__(self, api_key: Optional[str] = None):
|
20 |
+
"""Initialize the DeepSeek provider with API key"""
|
21 |
+
self.api_key = api_key or os.getenv("DEEPSEEK_API_KEY")
|
22 |
+
if not self.api_key:
|
23 |
+
logger.warning("No DeepSeek API key provided. Set DEEPSEEK_API_KEY env variable.")
|
24 |
+
|
25 |
+
self.base_url = "https://api.deepseek.com/v1"
|
26 |
+
self.headers = {
|
27 |
+
"Authorization": f"Bearer {self.api_key}",
|
28 |
+
"Content-Type": "application/json"
|
29 |
+
}
|
30 |
+
|
31 |
+
def generate_text(self,
|
32 |
+
prompt: str,
|
33 |
+
model: str = "deepseek-chat",
|
34 |
+
max_tokens: int = 1000,
|
35 |
+
temperature: float = 0.7,
|
36 |
+
system_message: str = "You are a helpful assistant.",
|
37 |
+
**kwargs) -> Dict[str, Any]:
|
38 |
+
"""Generate text using DeepSeek models"""
|
39 |
+
if not self.api_key:
|
40 |
+
return {"success": False, "error": "DeepSeek API key not provided"}
|
41 |
+
|
42 |
+
start_time = time.time()
|
43 |
+
|
44 |
+
try:
|
45 |
+
messages = [
|
46 |
+
{"role": "system", "content": system_message},
|
47 |
+
{"role": "user", "content": prompt}
|
48 |
+
]
|
49 |
+
|
50 |
+
payload = {
|
51 |
+
"model": model,
|
52 |
+
"messages": messages,
|
53 |
+
"max_tokens": max_tokens,
|
54 |
+
"temperature": temperature,
|
55 |
+
**kwargs
|
56 |
+
}
|
57 |
+
|
58 |
+
response = requests.post(
|
59 |
+
f"{self.base_url}/chat/completions",
|
60 |
+
headers=self.headers,
|
61 |
+
json=payload
|
62 |
+
)
|
63 |
+
|
64 |
+
# Check for errors
|
65 |
+
if response.status_code != 200:
|
66 |
+
logger.error(f"Error from DeepSeek API: {response.status_code} - {response.text}")
|
67 |
+
return {
|
68 |
+
"success": False,
|
69 |
+
"error": f"DeepSeek API error: {response.status_code}",
|
70 |
+
"response_time": time.time() - start_time,
|
71 |
+
"model": model,
|
72 |
+
"provider": "deepseek"
|
73 |
+
}
|
74 |
+
|
75 |
+
result = response.json()
|
76 |
+
|
77 |
+
# Extract the generated text
|
78 |
+
generated_text = result["choices"][0]["message"]["content"]
|
79 |
+
|
80 |
+
return {
|
81 |
+
"success": True,
|
82 |
+
"text": generated_text,
|
83 |
+
"model": model,
|
84 |
+
"provider": "deepseek",
|
85 |
+
"response_time": time.time() - start_time,
|
86 |
+
"tokens": {
|
87 |
+
"prompt": result.get("usage", {}).get("prompt_tokens", 0),
|
88 |
+
"completion": result.get("usage", {}).get("completion_tokens", 0),
|
89 |
+
"total": result.get("usage", {}).get("total_tokens", 0)
|
90 |
+
},
|
91 |
+
"raw_response": result
|
92 |
+
}
|
93 |
+
|
94 |
+
except Exception as e:
|
95 |
+
logger.error(f"Error generating text with DeepSeek: {e}")
|
96 |
+
return {
|
97 |
+
"success": False,
|
98 |
+
"error": str(e),
|
99 |
+
"response_time": time.time() - start_time,
|
100 |
+
"model": model,
|
101 |
+
"provider": "deepseek"
|
102 |
+
}
|
103 |
+
|
104 |
+
def generate_code(self,
|
105 |
+
prompt: str,
|
106 |
+
model: str = "deepseek-coder",
|
107 |
+
max_tokens: int = 2000,
|
108 |
+
temperature: float = 0.5,
|
109 |
+
**kwargs) -> Dict[str, Any]:
|
110 |
+
"""Generate code using DeepSeek Coder models"""
|
111 |
+
if not self.api_key:
|
112 |
+
return {"success": False, "error": "DeepSeek API key not provided"}
|
113 |
+
|
114 |
+
start_time = time.time()
|
115 |
+
|
116 |
+
try:
|
117 |
+
messages = [
|
118 |
+
{"role": "system", "content": "You are a helpful coding assistant."},
|
119 |
+
{"role": "user", "content": prompt}
|
120 |
+
]
|
121 |
+
|
122 |
+
payload = {
|
123 |
+
"model": model,
|
124 |
+
"messages": messages,
|
125 |
+
"max_tokens": max_tokens,
|
126 |
+
"temperature": temperature,
|
127 |
+
**kwargs
|
128 |
+
}
|
129 |
+
|
130 |
+
response = requests.post(
|
131 |
+
f"{self.base_url}/chat/completions",
|
132 |
+
headers=self.headers,
|
133 |
+
json=payload
|
134 |
+
)
|
135 |
+
|
136 |
+
# Check for errors
|
137 |
+
if response.status_code != 200:
|
138 |
+
logger.error(f"Error from DeepSeek API: {response.status_code} - {response.text}")
|
139 |
+
return {
|
140 |
+
"success": False,
|
141 |
+
"error": f"DeepSeek API error: {response.status_code}",
|
142 |
+
"response_time": time.time() - start_time,
|
143 |
+
"model": model,
|
144 |
+
"provider": "deepseek"
|
145 |
+
}
|
146 |
+
|
147 |
+
result = response.json()
|
148 |
+
|
149 |
+
# Extract the generated code
|
150 |
+
generated_code = result["choices"][0]["message"]["content"]
|
151 |
+
|
152 |
+
return {
|
153 |
+
"success": True,
|
154 |
+
"text": generated_code,
|
155 |
+
"model": model,
|
156 |
+
"provider": "deepseek",
|
157 |
+
"response_time": time.time() - start_time,
|
158 |
+
"tokens": {
|
159 |
+
"prompt": result.get("usage", {}).get("prompt_tokens", 0),
|
160 |
+
"completion": result.get("usage", {}).get("completion_tokens", 0),
|
161 |
+
"total": result.get("usage", {}).get("total_tokens", 0)
|
162 |
+
},
|
163 |
+
"raw_response": result
|
164 |
+
}
|
165 |
+
|
166 |
+
except Exception as e:
|
167 |
+
logger.error(f"Error generating code with DeepSeek: {e}")
|
168 |
+
return {
|
169 |
+
"success": False,
|
170 |
+
"error": str(e),
|
171 |
+
"response_time": time.time() - start_time,
|
172 |
+
"model": model,
|
173 |
+
"provider": "deepseek"
|
174 |
+
}
|
175 |
+
|
176 |
+
def get_available_models(self) -> List[Dict[str, Any]]:
|
177 |
+
"""Get available DeepSeek models"""
|
178 |
+
if not self.api_key:
|
179 |
+
return []
|
180 |
+
|
181 |
+
# DeepSeek doesn't have a list models endpoint yet, so we hardcode the currently available models
|
182 |
+
models = [
|
183 |
+
{
|
184 |
+
"id": "deepseek-chat",
|
185 |
+
"name": "DeepSeek Chat",
|
186 |
+
"description": "General-purpose language model for chat",
|
187 |
+
"context_length": 4096
|
188 |
+
},
|
189 |
+
{
|
190 |
+
"id": "deepseek-coder",
|
191 |
+
"name": "DeepSeek Coder",
|
192 |
+
"description": "Specialized model for code generation",
|
193 |
+
"context_length": 8192
|
194 |
+
},
|
195 |
+
{
|
196 |
+
"id": "deepseek-math",
|
197 |
+
"name": "DeepSeek Math",
|
198 |
+
"description": "Model fine-tuned for mathematical reasoning",
|
199 |
+
"context_length": 4096
|
200 |
+
}
|
201 |
+
]
|
202 |
+
|
203 |
+
return models
|
204 |
+
|
205 |
+
# Example usage
|
206 |
+
if __name__ == "__main__":
|
207 |
+
# Test the provider
|
208 |
+
provider = DeepSeekProvider()
|
209 |
+
result = provider.generate_text("Write a short poem about AI.")
|
210 |
+
print(json.dumps(result, indent=2))
|
providers/huggingface.py
ADDED
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
Hugging Face Provider Integration
|
3 |
+
Handles API calls to Hugging Face for AI model inference
|
4 |
+
"""
|
5 |
+
import os
|
6 |
+
import requests
|
7 |
+
import time
|
8 |
+
import json
|
9 |
+
import logging
|
10 |
+
from typing import Dict, Any, Optional, List
|
11 |
+
|
12 |
+
# Setup logging
|
13 |
+
logging.basicConfig(level=logging.INFO)
|
14 |
+
logger = logging.getLogger("huggingface")
|
15 |
+
|
16 |
+
class HuggingFaceProvider:
|
17 |
+
"""Hugging Face API provider for model inference"""
|
18 |
+
|
19 |
+
def __init__(self, api_key: Optional[str] = None):
|
20 |
+
"""Initialize the Hugging Face provider with API key"""
|
21 |
+
self.api_key = api_key or os.getenv("HUGGINGFACE_API_KEY")
|
22 |
+
if not self.api_key:
|
23 |
+
logger.warning("No Hugging Face API key provided. Set HUGGINGFACE_API_KEY env variable.")
|
24 |
+
|
25 |
+
self.base_url = "https://api-inference.huggingface.co/models"
|
26 |
+
self.headers = {"Authorization": f"Bearer {self.api_key}"} if self.api_key else {}
|
27 |
+
|
28 |
+
def generate_text(self,
|
29 |
+
prompt: str,
|
30 |
+
model: str = "mistralai/Mistral-7B-Instruct-v0.2",
|
31 |
+
max_tokens: int = 1000,
|
32 |
+
temperature: float = 0.7,
|
33 |
+
**kwargs) -> Dict[str, Any]:
|
34 |
+
"""Generate text using Hugging Face text generation models"""
|
35 |
+
start_time = time.time()
|
36 |
+
|
37 |
+
try:
|
38 |
+
url = f"{self.base_url}/{model}"
|
39 |
+
payload = {
|
40 |
+
"inputs": prompt,
|
41 |
+
"parameters": {
|
42 |
+
"max_new_tokens": max_tokens,
|
43 |
+
"temperature": temperature,
|
44 |
+
"return_full_text": False,
|
45 |
+
**kwargs
|
46 |
+
}
|
47 |
+
}
|
48 |
+
|
49 |
+
response = requests.post(
|
50 |
+
url,
|
51 |
+
headers=self.headers,
|
52 |
+
json=payload
|
53 |
+
)
|
54 |
+
|
55 |
+
# Check for errors
|
56 |
+
if response.status_code != 200:
|
57 |
+
logger.error(f"Error from Hugging Face API: {response.status_code} - {response.text}")
|
58 |
+
return {
|
59 |
+
"success": False,
|
60 |
+
"error": f"Hugging Face API error: {response.status_code}",
|
61 |
+
"response_time": time.time() - start_time,
|
62 |
+
"model": model,
|
63 |
+
"provider": "huggingface"
|
64 |
+
}
|
65 |
+
|
66 |
+
result = response.json()
|
67 |
+
|
68 |
+
# Handle different response formats
|
69 |
+
generated_text = ""
|
70 |
+
if isinstance(result, list) and len(result) > 0:
|
71 |
+
if "generated_text" in result[0]:
|
72 |
+
generated_text = result[0]["generated_text"]
|
73 |
+
else:
|
74 |
+
generated_text = result[0].get("text", "")
|
75 |
+
elif "generated_text" in result:
|
76 |
+
generated_text = result["generated_text"]
|
77 |
+
|
78 |
+
return {
|
79 |
+
"success": True,
|
80 |
+
"text": generated_text,
|
81 |
+
"model": model,
|
82 |
+
"provider": "huggingface",
|
83 |
+
"response_time": time.time() - start_time,
|
84 |
+
"raw_response": result
|
85 |
+
}
|
86 |
+
|
87 |
+
except Exception as e:
|
88 |
+
logger.error(f"Error generating text with Hugging Face: {e}")
|
89 |
+
return {
|
90 |
+
"success": False,
|
91 |
+
"error": str(e),
|
92 |
+
"response_time": time.time() - start_time,
|
93 |
+
"model": model,
|
94 |
+
"provider": "huggingface"
|
95 |
+
}
|
96 |
+
|
97 |
+
def generate_image(self,
|
98 |
+
prompt: str,
|
99 |
+
model: str = "stabilityai/stable-diffusion-xl-base-1.0",
|
100 |
+
height: int = 512,
|
101 |
+
width: int = 512,
|
102 |
+
**kwargs) -> Dict[str, Any]:
|
103 |
+
"""Generate image using Hugging Face image generation models"""
|
104 |
+
start_time = time.time()
|
105 |
+
|
106 |
+
try:
|
107 |
+
url = f"{self.base_url}/{model}"
|
108 |
+
payload = {
|
109 |
+
"inputs": prompt,
|
110 |
+
"parameters": {
|
111 |
+
"height": height,
|
112 |
+
"width": width,
|
113 |
+
**kwargs
|
114 |
+
}
|
115 |
+
}
|
116 |
+
|
117 |
+
response = requests.post(
|
118 |
+
url,
|
119 |
+
headers=self.headers,
|
120 |
+
json=payload
|
121 |
+
)
|
122 |
+
|
123 |
+
# Image response is binary
|
124 |
+
if response.status_code != 200:
|
125 |
+
logger.error(f"Error from Hugging Face API: {response.status_code} - {response.text}")
|
126 |
+
return {
|
127 |
+
"success": False,
|
128 |
+
"error": f"Hugging Face API error: {response.status_code}",
|
129 |
+
"response_time": time.time() - start_time,
|
130 |
+
"model": model,
|
131 |
+
"provider": "huggingface"
|
132 |
+
}
|
133 |
+
|
134 |
+
# Return binary image data in base64
|
135 |
+
import base64
|
136 |
+
image_data = base64.b64encode(response.content).decode("utf-8")
|
137 |
+
|
138 |
+
return {
|
139 |
+
"success": True,
|
140 |
+
"image_data": image_data,
|
141 |
+
"model": model,
|
142 |
+
"provider": "huggingface",
|
143 |
+
"response_time": time.time() - start_time
|
144 |
+
}
|
145 |
+
|
146 |
+
except Exception as e:
|
147 |
+
logger.error(f"Error generating image with Hugging Face: {e}")
|
148 |
+
return {
|
149 |
+
"success": False,
|
150 |
+
"error": str(e),
|
151 |
+
"response_time": time.time() - start_time,
|
152 |
+
"model": model,
|
153 |
+
"provider": "huggingface"
|
154 |
+
}
|
155 |
+
|
156 |
+
def get_available_models(self, task: str = "text-generation") -> List[Dict[str, Any]]:
|
157 |
+
"""Get available models for a specific task"""
|
158 |
+
try:
|
159 |
+
url = "https://huggingface.co/api/models"
|
160 |
+
params = {
|
161 |
+
"filter": task,
|
162 |
+
"sort": "downloads",
|
163 |
+
"direction": -1,
|
164 |
+
"limit": 100
|
165 |
+
}
|
166 |
+
|
167 |
+
response = requests.get(url, params=params)
|
168 |
+
|
169 |
+
if response.status_code != 200:
|
170 |
+
logger.error(f"Error fetching models: {response.status_code} - {response.text}")
|
171 |
+
return []
|
172 |
+
|
173 |
+
models = response.json()
|
174 |
+
return [
|
175 |
+
{
|
176 |
+
"id": model["id"],
|
177 |
+
"name": model.get("name", model["id"]),
|
178 |
+
"downloads": model.get("downloads", 0),
|
179 |
+
"tags": model.get("tags", [])
|
180 |
+
}
|
181 |
+
for model in models
|
182 |
+
]
|
183 |
+
|
184 |
+
except Exception as e:
|
185 |
+
logger.error(f"Error fetching models: {e}")
|
186 |
+
return []
|
187 |
+
|
188 |
+
# Example usage
|
189 |
+
if __name__ == "__main__":
|
190 |
+
# Test the provider
|
191 |
+
provider = HuggingFaceProvider()
|
192 |
+
result = provider.generate_text("Write a short poem about AI.")
|
193 |
+
print(json.dumps(result, indent=2))
|
providers/openai.py
ADDED
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
OpenAI Provider Integration
|
3 |
+
Handles API calls to OpenAI for text and image generation
|
4 |
+
"""
|
5 |
+
import os
|
6 |
+
import time
|
7 |
+
import json
|
8 |
+
import logging
|
9 |
+
from typing import Dict, Any, Optional, List
|
10 |
+
|
11 |
+
# Setup logging
|
12 |
+
logging.basicConfig(level=logging.INFO)
|
13 |
+
logger = logging.getLogger("openai")
|
14 |
+
|
15 |
+
try:
|
16 |
+
import openai
|
17 |
+
from openai import OpenAI
|
18 |
+
HAS_OPENAI = True
|
19 |
+
except ImportError:
|
20 |
+
logger.warning("OpenAI package not installed. Install with: pip install openai")
|
21 |
+
HAS_OPENAI = False
|
22 |
+
|
23 |
+
class OpenAIProvider:
|
24 |
+
"""OpenAI API provider for model inference"""
|
25 |
+
|
26 |
+
def __init__(self, api_key: Optional[str] = None):
|
27 |
+
"""Initialize the OpenAI provider with API key"""
|
28 |
+
if not HAS_OPENAI:
|
29 |
+
logger.error("OpenAI package not installed. Install with: pip install openai")
|
30 |
+
return
|
31 |
+
|
32 |
+
self.api_key = api_key or os.getenv("OPENAI_API_KEY")
|
33 |
+
if not self.api_key:
|
34 |
+
logger.warning("No OpenAI API key provided. Set OPENAI_API_KEY env variable.")
|
35 |
+
|
36 |
+
# Initialize client
|
37 |
+
self.client = OpenAI(api_key=self.api_key)
|
38 |
+
|
39 |
+
def generate_text(self,
|
40 |
+
prompt: str,
|
41 |
+
model: str = "gpt-3.5-turbo",
|
42 |
+
max_tokens: int = 1000,
|
43 |
+
temperature: float = 0.7,
|
44 |
+
system_message: str = "You are a helpful assistant.",
|
45 |
+
**kwargs) -> Dict[str, Any]:
|
46 |
+
"""Generate text using OpenAI models"""
|
47 |
+
if not HAS_OPENAI or not self.api_key:
|
48 |
+
return {"success": False, "error": "OpenAI package not installed or API key not provided"}
|
49 |
+
|
50 |
+
start_time = time.time()
|
51 |
+
|
52 |
+
try:
|
53 |
+
messages = [
|
54 |
+
{"role": "system", "content": system_message},
|
55 |
+
{"role": "user", "content": prompt}
|
56 |
+
]
|
57 |
+
|
58 |
+
response = self.client.chat.completions.create(
|
59 |
+
model=model,
|
60 |
+
messages=messages,
|
61 |
+
max_tokens=max_tokens,
|
62 |
+
temperature=temperature,
|
63 |
+
**kwargs
|
64 |
+
)
|
65 |
+
|
66 |
+
# Extract the generated text
|
67 |
+
generated_text = response.choices[0].message.content
|
68 |
+
|
69 |
+
return {
|
70 |
+
"success": True,
|
71 |
+
"text": generated_text,
|
72 |
+
"model": model,
|
73 |
+
"provider": "openai",
|
74 |
+
"response_time": time.time() - start_time,
|
75 |
+
"tokens": {
|
76 |
+
"prompt": response.usage.prompt_tokens,
|
77 |
+
"completion": response.usage.completion_tokens,
|
78 |
+
"total": response.usage.total_tokens
|
79 |
+
},
|
80 |
+
"raw_response": response.model_dump()
|
81 |
+
}
|
82 |
+
|
83 |
+
except Exception as e:
|
84 |
+
logger.error(f"Error generating text with OpenAI: {e}")
|
85 |
+
return {
|
86 |
+
"success": False,
|
87 |
+
"error": str(e),
|
88 |
+
"response_time": time.time() - start_time,
|
89 |
+
"model": model,
|
90 |
+
"provider": "openai"
|
91 |
+
}
|
92 |
+
|
93 |
+
def generate_image(self,
|
94 |
+
prompt: str,
|
95 |
+
model: str = "dall-e-3",
|
96 |
+
size: str = "1024x1024",
|
97 |
+
quality: str = "standard",
|
98 |
+
n: int = 1,
|
99 |
+
**kwargs) -> Dict[str, Any]:
|
100 |
+
"""Generate image using OpenAI DALL-E models"""
|
101 |
+
if not HAS_OPENAI or not self.api_key:
|
102 |
+
return {"success": False, "error": "OpenAI package not installed or API key not provided"}
|
103 |
+
|
104 |
+
start_time = time.time()
|
105 |
+
|
106 |
+
try:
|
107 |
+
response = self.client.images.generate(
|
108 |
+
model=model,
|
109 |
+
prompt=prompt,
|
110 |
+
size=size,
|
111 |
+
quality=quality,
|
112 |
+
n=n,
|
113 |
+
**kwargs
|
114 |
+
)
|
115 |
+
|
116 |
+
return {
|
117 |
+
"success": True,
|
118 |
+
"image_url": response.data[0].url, # URL of the generated image
|
119 |
+
"model": model,
|
120 |
+
"provider": "openai",
|
121 |
+
"response_time": time.time() - start_time,
|
122 |
+
"raw_response": response.model_dump()
|
123 |
+
}
|
124 |
+
|
125 |
+
except Exception as e:
|
126 |
+
logger.error(f"Error generating image with OpenAI: {e}")
|
127 |
+
return {
|
128 |
+
"success": False,
|
129 |
+
"error": str(e),
|
130 |
+
"response_time": time.time() - start_time,
|
131 |
+
"model": model,
|
132 |
+
"provider": "openai"
|
133 |
+
}
|
134 |
+
|
135 |
+
def get_available_models(self) -> List[Dict[str, Any]]:
|
136 |
+
"""Get available OpenAI models"""
|
137 |
+
if not HAS_OPENAI or not self.api_key:
|
138 |
+
return []
|
139 |
+
|
140 |
+
try:
|
141 |
+
response = self.client.models.list()
|
142 |
+
|
143 |
+
models = [
|
144 |
+
{
|
145 |
+
"id": model.id,
|
146 |
+
"name": model.id,
|
147 |
+
"created": model.created
|
148 |
+
}
|
149 |
+
for model in response.data
|
150 |
+
]
|
151 |
+
|
152 |
+
# Filter to only include completion and chat models
|
153 |
+
text_models = [
|
154 |
+
model for model in models
|
155 |
+
if any(prefix in model["id"] for prefix in ["gpt-", "text-"])
|
156 |
+
]
|
157 |
+
|
158 |
+
# Add DALL-E models (they don't show up in the list)
|
159 |
+
image_models = [
|
160 |
+
{"id": "dall-e-3", "name": "DALL-E 3"},
|
161 |
+
{"id": "dall-e-2", "name": "DALL-E 2"}
|
162 |
+
]
|
163 |
+
|
164 |
+
return {
|
165 |
+
"text_models": text_models,
|
166 |
+
"image_models": image_models
|
167 |
+
}
|
168 |
+
|
169 |
+
except Exception as e:
|
170 |
+
logger.error(f"Error fetching OpenAI models: {e}")
|
171 |
+
return []
|
172 |
+
|
173 |
+
# Example usage
|
174 |
+
if __name__ == "__main__":
|
175 |
+
# Test the provider
|
176 |
+
provider = OpenAIProvider()
|
177 |
+
result = provider.generate_text("Write a short poem about AI.")
|
178 |
+
print(json.dumps(result, indent=2))
|
providers/openrouter.py
ADDED
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
OpenRouter Provider Integration
|
3 |
+
Handles API calls to OpenRouter for AI model inference across multiple providers
|
4 |
+
"""
|
5 |
+
import os
|
6 |
+
import requests
|
7 |
+
import time
|
8 |
+
import json
|
9 |
+
import logging
|
10 |
+
from typing import Dict, Any, Optional, List
|
11 |
+
|
12 |
+
# Setup logging
|
13 |
+
logging.basicConfig(level=logging.INFO)
|
14 |
+
logger = logging.getLogger("openrouter")
|
15 |
+
|
16 |
+
class OpenRouterProvider:
|
17 |
+
"""OpenRouter API provider for model inference across multiple AI providers"""
|
18 |
+
|
19 |
+
def __init__(self, api_key: Optional[str] = None):
|
20 |
+
"""Initialize the OpenRouter provider with API key"""
|
21 |
+
self.api_key = api_key or os.getenv("OPENROUTER_API_KEY")
|
22 |
+
if not self.api_key:
|
23 |
+
logger.warning("No OpenRouter API key provided. Set OPENROUTER_API_KEY env variable.")
|
24 |
+
|
25 |
+
self.base_url = "https://openrouter.ai/api/v1"
|
26 |
+
self.headers = {
|
27 |
+
"Authorization": f"Bearer {self.api_key}",
|
28 |
+
"Content-Type": "application/json",
|
29 |
+
"HTTP-Referer": os.getenv("APP_URL", "http://localhost:8000"), # Required by OpenRouter
|
30 |
+
"X-Title": os.getenv("APP_NAME", "AI Tool Hub") # Your app name
|
31 |
+
}
|
32 |
+
|
33 |
+
def generate_text(self,
|
34 |
+
prompt: str,
|
35 |
+
model: str = "anthropic/claude-3-opus:beta",
|
36 |
+
max_tokens: int = 1000,
|
37 |
+
temperature: float = 0.7,
|
38 |
+
system_message: str = "You are a helpful assistant.",
|
39 |
+
**kwargs) -> Dict[str, Any]:
|
40 |
+
"""Generate text using OpenRouter models"""
|
41 |
+
if not self.api_key:
|
42 |
+
return {"success": False, "error": "OpenRouter API key not provided"}
|
43 |
+
|
44 |
+
start_time = time.time()
|
45 |
+
|
46 |
+
try:
|
47 |
+
messages = [
|
48 |
+
{"role": "system", "content": system_message},
|
49 |
+
{"role": "user", "content": prompt}
|
50 |
+
]
|
51 |
+
|
52 |
+
payload = {
|
53 |
+
"model": model,
|
54 |
+
"messages": messages,
|
55 |
+
"max_tokens": max_tokens,
|
56 |
+
"temperature": temperature,
|
57 |
+
**kwargs
|
58 |
+
}
|
59 |
+
|
60 |
+
response = requests.post(
|
61 |
+
f"{self.base_url}/chat/completions",
|
62 |
+
headers=self.headers,
|
63 |
+
json=payload
|
64 |
+
)
|
65 |
+
|
66 |
+
# Check for errors
|
67 |
+
if response.status_code != 200:
|
68 |
+
logger.error(f"Error from OpenRouter API: {response.status_code} - {response.text}")
|
69 |
+
return {
|
70 |
+
"success": False,
|
71 |
+
"error": f"OpenRouter API error: {response.status_code}",
|
72 |
+
"response_time": time.time() - start_time,
|
73 |
+
"model": model,
|
74 |
+
"provider": "openrouter"
|
75 |
+
}
|
76 |
+
|
77 |
+
result = response.json()
|
78 |
+
|
79 |
+
# Extract the generated text
|
80 |
+
generated_text = result["choices"][0]["message"]["content"]
|
81 |
+
|
82 |
+
return {
|
83 |
+
"success": True,
|
84 |
+
"text": generated_text,
|
85 |
+
"model": model,
|
86 |
+
"provider": "openrouter",
|
87 |
+
"response_time": time.time() - start_time,
|
88 |
+
"tokens": {
|
89 |
+
"prompt": result.get("usage", {}).get("prompt_tokens", 0),
|
90 |
+
"completion": result.get("usage", {}).get("completion_tokens", 0),
|
91 |
+
"total": result.get("usage", {}).get("total_tokens", 0)
|
92 |
+
},
|
93 |
+
"raw_response": result
|
94 |
+
}
|
95 |
+
|
96 |
+
except Exception as e:
|
97 |
+
logger.error(f"Error generating text with OpenRouter: {e}")
|
98 |
+
return {
|
99 |
+
"success": False,
|
100 |
+
"error": str(e),
|
101 |
+
"response_time": time.time() - start_time,
|
102 |
+
"model": model,
|
103 |
+
"provider": "openrouter"
|
104 |
+
}
|
105 |
+
|
106 |
+
def get_available_models(self) -> List[Dict[str, Any]]:
|
107 |
+
"""Get available OpenRouter models"""
|
108 |
+
if not self.api_key:
|
109 |
+
return []
|
110 |
+
|
111 |
+
try:
|
112 |
+
response = requests.get(
|
113 |
+
f"{self.base_url}/models",
|
114 |
+
headers=self.headers
|
115 |
+
)
|
116 |
+
|
117 |
+
if response.status_code != 200:
|
118 |
+
logger.error(f"Error fetching OpenRouter models: {response.status_code} - {response.text}")
|
119 |
+
return []
|
120 |
+
|
121 |
+
models_data = response.json()
|
122 |
+
models = []
|
123 |
+
|
124 |
+
for model in models_data.get("data", []):
|
125 |
+
models.append({
|
126 |
+
"id": model.get("id"),
|
127 |
+
"name": model.get("name", model.get("id")),
|
128 |
+
"description": model.get("description", ""),
|
129 |
+
"context_length": model.get("context_length", 4096),
|
130 |
+
"pricing": {
|
131 |
+
"prompt": model.get("pricing", {}).get("prompt", 0),
|
132 |
+
"completion": model.get("pricing", {}).get("completion", 0)
|
133 |
+
}
|
134 |
+
})
|
135 |
+
|
136 |
+
return models
|
137 |
+
|
138 |
+
except Exception as e:
|
139 |
+
logger.error(f"Error fetching OpenRouter models: {e}")
|
140 |
+
return []
|
141 |
+
|
142 |
+
def get_models_by_provider(self, provider: str = None) -> List[Dict[str, Any]]:
|
143 |
+
"""Get available models filtered by provider"""
|
144 |
+
models = self.get_available_models()
|
145 |
+
|
146 |
+
if not provider:
|
147 |
+
return models
|
148 |
+
|
149 |
+
return [model for model in models if provider.lower() in model.get("id", "").lower()]
|
150 |
+
|
151 |
+
# Example usage
|
152 |
+
if __name__ == "__main__":
|
153 |
+
# Test the provider
|
154 |
+
provider = OpenRouterProvider()
|
155 |
+
result = provider.generate_text("Write a short poem about AI.")
|
156 |
+
print(json.dumps(result, indent=2))
|
157 |
+
|
158 |
+
# Get all models
|
159 |
+
models = provider.get_available_models()
|
160 |
+
print(f"Found {len(models)} models")
|
161 |
+
|
162 |
+
# Get only Anthropic models
|
163 |
+
anthropic_models = provider.get_models_by_provider("anthropic")
|
164 |
+
print(f"Found {len(anthropic_models)} Anthropic models")
|
requirements.txt
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Web Framework
|
2 |
+
fastapi==0.115.0
|
3 |
+
uvicorn==0.24.0
|
4 |
+
jinja2==3.1.2
|
5 |
+
python-multipart==0.0.6
|
6 |
+
pydantic==2.4.2
|
7 |
+
python-dotenv==1.0.0
|
8 |
+
itsdangerous==2.1.2
|
9 |
+
aiofiles==23.2.1
|
10 |
+
requests==2.31.0
|
11 |
+
# starlette is included as a dependency of FastAPI
|
12 |
+
|
13 |
+
# Database
|
14 |
+
boto3==1.28.48
|
15 |
+
redis==5.0.0
|
16 |
+
|
17 |
+
# AI/ML
|
18 |
+
openai==1.0.0
|
19 |
+
|
20 |
+
# Authentication
|
21 |
+
python-jose[cryptography]>=3.3.0
|
22 |
+
passlib[bcrypt]>=1.7.4
|
23 |
+
PyJWT==2.8.0
|
24 |
+
|
25 |
+
# Utilities
|
26 |
+
pyyaml>=6.0.0
|
27 |
+
psutil==5.9.5
|
28 |
+
|
29 |
+
# Testing
|
30 |
+
pytest>=7.0.0
|
31 |
+
pytest-asyncio>=0.18.0
|
32 |
+
|
33 |
+
# Additional dependencies
|
34 |
+
pandas==2.1.0
|
35 |
+
huggingface-hub==0.17.2
|
static/css/styles.css
ADDED
@@ -0,0 +1,737 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* MegicAI - CSS styles
|
2 |
+
* Main stylesheet for the single-process FastAPI application
|
3 |
+
*/
|
4 |
+
|
5 |
+
/* === Base Styles === */
|
6 |
+
:root {
|
7 |
+
--primary-color: #121212;
|
8 |
+
--secondary-color: #7367F0;
|
9 |
+
--accent-color: #FFD700;
|
10 |
+
--text-color: #FFFFFF;
|
11 |
+
--text-dark: #333333;
|
12 |
+
--card-bg: rgba(30, 30, 40, 0.85);
|
13 |
+
--hover-color: rgba(255, 255, 255, 0.15);
|
14 |
+
--sidebar-bg: rgba(21, 21, 31, 0.95);
|
15 |
+
--sidebar-hover: rgba(255, 255, 255, 0.1);
|
16 |
+
--input-bg: rgba(45, 45, 60, 0.8);
|
17 |
+
--button-primary-bg: linear-gradient(135deg, #7367F0, #8E54E9);
|
18 |
+
--button-secondary-bg: rgba(70, 70, 85, 0.5);
|
19 |
+
--header-bg: rgba(25, 25, 35, 0.9);
|
20 |
+
--tooltip-bg: rgba(10, 10, 15, 0.95);
|
21 |
+
--dark-sidebar: #1a1a1a;
|
22 |
+
--success-color: #4CAF50;
|
23 |
+
--warning-color: #FFC107;
|
24 |
+
--error-color: #F44336;
|
25 |
+
--info-color: #2196F3;
|
26 |
+
}
|
27 |
+
|
28 |
+
* {
|
29 |
+
margin: 0;
|
30 |
+
padding: 0;
|
31 |
+
box-sizing: border-box;
|
32 |
+
}
|
33 |
+
|
34 |
+
body {
|
35 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
36 |
+
line-height: 1.6;
|
37 |
+
color: var(--text-color);
|
38 |
+
background-color: var(--primary-color);
|
39 |
+
min-height: 100vh;
|
40 |
+
}
|
41 |
+
|
42 |
+
a {
|
43 |
+
color: var(--text-color);
|
44 |
+
text-decoration: none;
|
45 |
+
}
|
46 |
+
|
47 |
+
h1, h2, h3, h4, h5, h6 {
|
48 |
+
margin-bottom: 0.5rem;
|
49 |
+
font-weight: 600;
|
50 |
+
}
|
51 |
+
|
52 |
+
/* === Layout === */
|
53 |
+
.app-container {
|
54 |
+
display: flex;
|
55 |
+
min-height: 100vh;
|
56 |
+
}
|
57 |
+
|
58 |
+
/* === Sidebar === */
|
59 |
+
.sidebar {
|
60 |
+
width: 260px;
|
61 |
+
background-color: var(--sidebar-bg);
|
62 |
+
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
63 |
+
display: flex;
|
64 |
+
flex-direction: column;
|
65 |
+
position: fixed;
|
66 |
+
height: 100vh;
|
67 |
+
overflow-y: auto;
|
68 |
+
}
|
69 |
+
|
70 |
+
.sidebar-header {
|
71 |
+
padding: 20px;
|
72 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
73 |
+
}
|
74 |
+
|
75 |
+
.sidebar-header h1 {
|
76 |
+
font-size: 1.5rem;
|
77 |
+
margin-bottom: 0.2rem;
|
78 |
+
}
|
79 |
+
|
80 |
+
.sidebar-subtitle {
|
81 |
+
opacity: 0.7;
|
82 |
+
font-size: 0.9rem;
|
83 |
+
}
|
84 |
+
|
85 |
+
.sidebar-section {
|
86 |
+
padding: 20px;
|
87 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
88 |
+
}
|
89 |
+
|
90 |
+
.sidebar-heading {
|
91 |
+
font-size: 0.8rem;
|
92 |
+
text-transform: uppercase;
|
93 |
+
letter-spacing: 1px;
|
94 |
+
opacity: 0.7;
|
95 |
+
margin-bottom: 15px;
|
96 |
+
}
|
97 |
+
|
98 |
+
.nav-item {
|
99 |
+
display: flex;
|
100 |
+
align-items: center;
|
101 |
+
padding: 10px;
|
102 |
+
border-radius: 5px;
|
103 |
+
margin-bottom: 5px;
|
104 |
+
transition: background-color 0.2s;
|
105 |
+
}
|
106 |
+
|
107 |
+
.nav-item:hover {
|
108 |
+
background-color: var(--sidebar-hover);
|
109 |
+
}
|
110 |
+
|
111 |
+
.nav-item.active {
|
112 |
+
background-color: var(--secondary-color);
|
113 |
+
}
|
114 |
+
|
115 |
+
.nav-icon {
|
116 |
+
margin-right: 12px;
|
117 |
+
font-size: 1.2rem;
|
118 |
+
}
|
119 |
+
|
120 |
+
.sidebar-footer {
|
121 |
+
margin-top: auto;
|
122 |
+
padding: 20px;
|
123 |
+
opacity: 0.5;
|
124 |
+
font-size: 0.8rem;
|
125 |
+
text-align: center;
|
126 |
+
}
|
127 |
+
|
128 |
+
/* === Content Area === */
|
129 |
+
.content {
|
130 |
+
flex: 1;
|
131 |
+
padding: 30px;
|
132 |
+
margin-left: 260px;
|
133 |
+
width: calc(100% - 260px);
|
134 |
+
}
|
135 |
+
|
136 |
+
.content-header {
|
137 |
+
margin-bottom: 30px;
|
138 |
+
}
|
139 |
+
|
140 |
+
.content-header h1 {
|
141 |
+
font-size: 2rem;
|
142 |
+
margin-bottom: 0.5rem;
|
143 |
+
}
|
144 |
+
|
145 |
+
/* === Cards & Tools === */
|
146 |
+
.tools-grid {
|
147 |
+
display: grid;
|
148 |
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
149 |
+
gap: 20px;
|
150 |
+
margin-top: 20px;
|
151 |
+
}
|
152 |
+
|
153 |
+
.tool-card {
|
154 |
+
background-color: var(--card-bg);
|
155 |
+
border-radius: 8px;
|
156 |
+
padding: 20px;
|
157 |
+
display: flex;
|
158 |
+
transition: transform 0.2s, box-shadow 0.2s;
|
159 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
160 |
+
}
|
161 |
+
|
162 |
+
.tool-card:hover {
|
163 |
+
transform: translateY(-3px);
|
164 |
+
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
165 |
+
}
|
166 |
+
|
167 |
+
.tool-icon {
|
168 |
+
font-size: 2rem;
|
169 |
+
margin-right: 15px;
|
170 |
+
display: flex;
|
171 |
+
align-items: center;
|
172 |
+
justify-content: center;
|
173 |
+
}
|
174 |
+
|
175 |
+
.tool-info {
|
176 |
+
flex: 1;
|
177 |
+
}
|
178 |
+
|
179 |
+
.tool-name {
|
180 |
+
font-size: 1.2rem;
|
181 |
+
margin-bottom: 5px;
|
182 |
+
}
|
183 |
+
|
184 |
+
.tool-description {
|
185 |
+
opacity: 0.8;
|
186 |
+
font-size: 0.9rem;
|
187 |
+
margin-bottom: 10px;
|
188 |
+
}
|
189 |
+
|
190 |
+
.tool-meta {
|
191 |
+
display: flex;
|
192 |
+
align-items: center;
|
193 |
+
margin-top: auto;
|
194 |
+
}
|
195 |
+
|
196 |
+
.tool-cost {
|
197 |
+
background-color: rgba(255, 215, 0, 0.2);
|
198 |
+
color: var(--accent-color);
|
199 |
+
padding: 2px 8px;
|
200 |
+
border-radius: 20px;
|
201 |
+
font-size: 0.8rem;
|
202 |
+
}
|
203 |
+
|
204 |
+
/* === Credits Display === */
|
205 |
+
.credit-display {
|
206 |
+
display: flex;
|
207 |
+
align-items: center;
|
208 |
+
background-color: rgba(255, 255, 255, 0.1);
|
209 |
+
padding: 12px;
|
210 |
+
border-radius: 8px;
|
211 |
+
}
|
212 |
+
|
213 |
+
/* === Auth Buttons & User Info === */
|
214 |
+
.auth-buttons {
|
215 |
+
display: flex;
|
216 |
+
gap: 10px;
|
217 |
+
margin-bottom: 15px;
|
218 |
+
}
|
219 |
+
|
220 |
+
.auth-buttons .btn {
|
221 |
+
flex: 1;
|
222 |
+
}
|
223 |
+
|
224 |
+
.user-section {
|
225 |
+
margin-bottom: 15px;
|
226 |
+
}
|
227 |
+
|
228 |
+
.user-info {
|
229 |
+
display: flex;
|
230 |
+
align-items: center;
|
231 |
+
margin-bottom: 12px;
|
232 |
+
}
|
233 |
+
|
234 |
+
.user-icon {
|
235 |
+
font-size: 1.5rem;
|
236 |
+
margin-right: 12px;
|
237 |
+
color: var(--secondary-color);
|
238 |
+
}
|
239 |
+
|
240 |
+
.user-name {
|
241 |
+
font-weight: 600;
|
242 |
+
}
|
243 |
+
|
244 |
+
.user-role {
|
245 |
+
font-size: 0.8rem;
|
246 |
+
opacity: 0.7;
|
247 |
+
}
|
248 |
+
|
249 |
+
.user-actions {
|
250 |
+
display: flex;
|
251 |
+
gap: 10px;
|
252 |
+
}
|
253 |
+
|
254 |
+
.btn-small {
|
255 |
+
padding: 5px 10px;
|
256 |
+
font-size: 0.8rem;
|
257 |
+
}
|
258 |
+
|
259 |
+
.credit-icon {
|
260 |
+
font-size: 1.5rem;
|
261 |
+
margin-right: 12px;
|
262 |
+
}
|
263 |
+
|
264 |
+
.credit-label {
|
265 |
+
font-size: 0.8rem;
|
266 |
+
opacity: 0.8;
|
267 |
+
}
|
268 |
+
|
269 |
+
.credit-value {
|
270 |
+
font-size: 1.2rem;
|
271 |
+
font-weight: 600;
|
272 |
+
}
|
273 |
+
|
274 |
+
/* === Forms & Inputs === */
|
275 |
+
.form-group {
|
276 |
+
margin-bottom: 20px;
|
277 |
+
}
|
278 |
+
|
279 |
+
.form-group label {
|
280 |
+
display: block;
|
281 |
+
margin-bottom: 8px;
|
282 |
+
font-weight: 500;
|
283 |
+
}
|
284 |
+
|
285 |
+
.form-select {
|
286 |
+
width: 100%;
|
287 |
+
padding: 10px;
|
288 |
+
background-color: var(--input-bg);
|
289 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
290 |
+
border-radius: 5px;
|
291 |
+
color: var(--text-color);
|
292 |
+
font-size: 1rem;
|
293 |
+
}
|
294 |
+
|
295 |
+
.prompt-textarea {
|
296 |
+
width: 100%;
|
297 |
+
min-height: 150px;
|
298 |
+
padding: 12px;
|
299 |
+
background-color: var(--input-bg);
|
300 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
301 |
+
border-radius: 5px;
|
302 |
+
color: var(--text-color);
|
303 |
+
font-size: 1rem;
|
304 |
+
resize: vertical;
|
305 |
+
}
|
306 |
+
|
307 |
+
.template-box {
|
308 |
+
background-color: rgba(0, 0, 0, 0.2);
|
309 |
+
padding: 12px;
|
310 |
+
border-radius: 5px;
|
311 |
+
margin-bottom: 10px;
|
312 |
+
font-size: 0.9rem;
|
313 |
+
}
|
314 |
+
|
315 |
+
/* === Buttons === */
|
316 |
+
.btn {
|
317 |
+
padding: 10px 20px;
|
318 |
+
border-radius: 5px;
|
319 |
+
border: none;
|
320 |
+
font-size: 1rem;
|
321 |
+
cursor: pointer;
|
322 |
+
transition: all 0.2s;
|
323 |
+
display: inline-block;
|
324 |
+
text-align: center;
|
325 |
+
}
|
326 |
+
|
327 |
+
.btn-primary {
|
328 |
+
background: var(--button-primary-bg);
|
329 |
+
color: white;
|
330 |
+
}
|
331 |
+
|
332 |
+
.btn-primary:hover {
|
333 |
+
opacity: 0.9;
|
334 |
+
transform: translateY(-2px);
|
335 |
+
}
|
336 |
+
|
337 |
+
.btn-secondary {
|
338 |
+
background-color: var(--button-secondary-bg);
|
339 |
+
color: white;
|
340 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
341 |
+
}
|
342 |
+
|
343 |
+
.btn-secondary:hover {
|
344 |
+
background-color: rgba(90, 90, 105, 0.5);
|
345 |
+
}
|
346 |
+
|
347 |
+
.btn:disabled {
|
348 |
+
opacity: 0.6;
|
349 |
+
cursor: not-allowed;
|
350 |
+
}
|
351 |
+
|
352 |
+
/* === Search === */
|
353 |
+
.search-container {
|
354 |
+
margin-bottom: 20px;
|
355 |
+
}
|
356 |
+
|
357 |
+
.search-input {
|
358 |
+
width: 100%;
|
359 |
+
padding: 12px 15px;
|
360 |
+
border-radius: 5px;
|
361 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
362 |
+
background-color: var(--input-bg);
|
363 |
+
color: var(--text-color);
|
364 |
+
font-size: 1rem;
|
365 |
+
}
|
366 |
+
|
367 |
+
/* === Tool Page === */
|
368 |
+
.tool-header {
|
369 |
+
display: flex;
|
370 |
+
align-items: center;
|
371 |
+
margin-bottom: 30px;
|
372 |
+
background-color: var(--header-bg);
|
373 |
+
padding: 20px;
|
374 |
+
border-radius: 10px;
|
375 |
+
}
|
376 |
+
|
377 |
+
.tool-header-icon {
|
378 |
+
font-size: 2.5rem;
|
379 |
+
margin-right: 20px;
|
380 |
+
}
|
381 |
+
|
382 |
+
.tool-content {
|
383 |
+
display: grid;
|
384 |
+
grid-template-columns: 3fr 1fr;
|
385 |
+
gap: 20px;
|
386 |
+
}
|
387 |
+
|
388 |
+
.tool-sidebar {
|
389 |
+
background-color: var(--card-bg);
|
390 |
+
border-radius: 8px;
|
391 |
+
padding: 20px;
|
392 |
+
height: fit-content;
|
393 |
+
}
|
394 |
+
|
395 |
+
.tool-cost-box h3 {
|
396 |
+
margin-bottom: 10px;
|
397 |
+
}
|
398 |
+
|
399 |
+
.cost-display {
|
400 |
+
background-color: rgba(255, 215, 0, 0.2);
|
401 |
+
padding: 10px;
|
402 |
+
border-radius: 5px;
|
403 |
+
text-align: center;
|
404 |
+
margin-bottom: 15px;
|
405 |
+
}
|
406 |
+
|
407 |
+
.cost-value {
|
408 |
+
font-size: 1.5rem;
|
409 |
+
font-weight: 600;
|
410 |
+
color: var(--accent-color);
|
411 |
+
}
|
412 |
+
|
413 |
+
.suggestion-chips {
|
414 |
+
display: flex;
|
415 |
+
flex-wrap: wrap;
|
416 |
+
gap: 8px;
|
417 |
+
margin-bottom: 20px;
|
418 |
+
}
|
419 |
+
|
420 |
+
.suggestion-chip {
|
421 |
+
background-color: var(--button-secondary-bg);
|
422 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
423 |
+
border-radius: 20px;
|
424 |
+
padding: 5px 15px;
|
425 |
+
font-size: 0.9rem;
|
426 |
+
cursor: pointer;
|
427 |
+
transition: all 0.2s;
|
428 |
+
}
|
429 |
+
|
430 |
+
.suggestion-chip:hover {
|
431 |
+
background-color: var(--secondary-color);
|
432 |
+
}
|
433 |
+
|
434 |
+
.button-section {
|
435 |
+
display: flex;
|
436 |
+
gap: 10px;
|
437 |
+
margin-top: 30px;
|
438 |
+
}
|
439 |
+
|
440 |
+
/* === Result Page === */
|
441 |
+
.result-header {
|
442 |
+
margin-bottom: 20px;
|
443 |
+
}
|
444 |
+
|
445 |
+
.result-meta {
|
446 |
+
display: flex;
|
447 |
+
gap: 20px;
|
448 |
+
margin-top: 10px;
|
449 |
+
}
|
450 |
+
|
451 |
+
.result-provider, .result-time {
|
452 |
+
display: flex;
|
453 |
+
align-items: center;
|
454 |
+
background-color: rgba(255, 255, 255, 0.1);
|
455 |
+
padding: 8px 15px;
|
456 |
+
border-radius: 20px;
|
457 |
+
font-size: 0.9rem;
|
458 |
+
}
|
459 |
+
|
460 |
+
.provider-icon, .time-icon {
|
461 |
+
margin-right: 8px;
|
462 |
+
}
|
463 |
+
|
464 |
+
.result-content {
|
465 |
+
background-color: var(--card-bg);
|
466 |
+
border-radius: 10px;
|
467 |
+
padding: 20px;
|
468 |
+
margin-bottom: 20px;
|
469 |
+
}
|
470 |
+
|
471 |
+
.result-text {
|
472 |
+
background-color: white;
|
473 |
+
color: var(--text-dark);
|
474 |
+
padding: 20px;
|
475 |
+
border-radius: 8px;
|
476 |
+
margin-bottom: 20px;
|
477 |
+
line-height: 1.6;
|
478 |
+
}
|
479 |
+
|
480 |
+
.result-image img {
|
481 |
+
max-width: 100%;
|
482 |
+
border-radius: 8px;
|
483 |
+
display: block;
|
484 |
+
margin: 0 auto;
|
485 |
+
}
|
486 |
+
|
487 |
+
.result-video video {
|
488 |
+
max-width: 100%;
|
489 |
+
border-radius: 8px;
|
490 |
+
display: block;
|
491 |
+
margin: 0 auto;
|
492 |
+
}
|
493 |
+
|
494 |
+
.result-ai-probability {
|
495 |
+
margin-top: 20px;
|
496 |
+
}
|
497 |
+
|
498 |
+
.probability-bar {
|
499 |
+
height: 20px;
|
500 |
+
background-color: rgba(255, 255, 255, 0.1);
|
501 |
+
border-radius: 10px;
|
502 |
+
overflow: hidden;
|
503 |
+
margin-bottom: 5px;
|
504 |
+
}
|
505 |
+
|
506 |
+
.probability-fill {
|
507 |
+
height: 100%;
|
508 |
+
width: var(--probability);
|
509 |
+
background: linear-gradient(90deg, #4CAF50, #F44336);
|
510 |
+
border-radius: 10px;
|
511 |
+
}
|
512 |
+
|
513 |
+
.probability-label {
|
514 |
+
display: flex;
|
515 |
+
justify-content: space-between;
|
516 |
+
font-size: 0.8rem;
|
517 |
+
opacity: 0.8;
|
518 |
+
}
|
519 |
+
|
520 |
+
.result-actions {
|
521 |
+
display: flex;
|
522 |
+
flex-direction: column;
|
523 |
+
gap: 20px;
|
524 |
+
}
|
525 |
+
|
526 |
+
.action-buttons {
|
527 |
+
display: flex;
|
528 |
+
gap: 10px;
|
529 |
+
}
|
530 |
+
|
531 |
+
.result-details {
|
532 |
+
background-color: rgba(0, 0, 0, 0.2);
|
533 |
+
border-radius: 5px;
|
534 |
+
padding: 10px;
|
535 |
+
}
|
536 |
+
|
537 |
+
.result-details summary {
|
538 |
+
cursor: pointer;
|
539 |
+
padding: 5px;
|
540 |
+
}
|
541 |
+
|
542 |
+
.response-json {
|
543 |
+
background-color: rgba(0, 0, 0, 0.3);
|
544 |
+
padding: 10px;
|
545 |
+
border-radius: 5px;
|
546 |
+
overflow-x: auto;
|
547 |
+
margin-top: 10px;
|
548 |
+
}
|
549 |
+
|
550 |
+
.response-json pre {
|
551 |
+
font-family: monospace;
|
552 |
+
font-size: 0.9rem;
|
553 |
+
white-space: pre-wrap;
|
554 |
+
}
|
555 |
+
|
556 |
+
/* === Ad Page === */
|
557 |
+
.ad-header {
|
558 |
+
margin-bottom: 20px;
|
559 |
+
}
|
560 |
+
|
561 |
+
.ad-content {
|
562 |
+
background-color: var(--card-bg);
|
563 |
+
border-radius: 10px;
|
564 |
+
padding: 20px;
|
565 |
+
}
|
566 |
+
|
567 |
+
.ad-info {
|
568 |
+
display: grid;
|
569 |
+
grid-template-columns: 1fr 1fr;
|
570 |
+
gap: 20px;
|
571 |
+
margin-bottom: 20px;
|
572 |
+
}
|
573 |
+
|
574 |
+
.ad-reward, .ad-duration {
|
575 |
+
display: flex;
|
576 |
+
align-items: center;
|
577 |
+
background-color: rgba(0, 0, 0, 0.2);
|
578 |
+
padding: 15px;
|
579 |
+
border-radius: 8px;
|
580 |
+
}
|
581 |
+
|
582 |
+
.reward-icon, .duration-icon {
|
583 |
+
font-size: 2rem;
|
584 |
+
margin-right: 15px;
|
585 |
+
}
|
586 |
+
|
587 |
+
.ad-player {
|
588 |
+
margin-bottom: 20px;
|
589 |
+
}
|
590 |
+
|
591 |
+
.ad-container {
|
592 |
+
background-color: #000;
|
593 |
+
aspect-ratio: 16 / 9;
|
594 |
+
border-radius: 8px;
|
595 |
+
overflow: hidden;
|
596 |
+
}
|
597 |
+
|
598 |
+
.mock-ad {
|
599 |
+
width: 100%;
|
600 |
+
height: 100%;
|
601 |
+
display: flex;
|
602 |
+
align-items: center;
|
603 |
+
justify-content: center;
|
604 |
+
color: white;
|
605 |
+
text-align: center;
|
606 |
+
}
|
607 |
+
|
608 |
+
.mock-ad-content {
|
609 |
+
max-width: 80%;
|
610 |
+
}
|
611 |
+
|
612 |
+
.ad-timer-container {
|
613 |
+
margin: 20px 0;
|
614 |
+
}
|
615 |
+
|
616 |
+
.ad-timer {
|
617 |
+
display: inline-block;
|
618 |
+
background-color: rgba(255, 255, 255, 0.2);
|
619 |
+
padding: 5px 15px;
|
620 |
+
border-radius: 20px;
|
621 |
+
font-weight: bold;
|
622 |
+
}
|
623 |
+
|
624 |
+
.mock-ad-image {
|
625 |
+
margin-top: 20px;
|
626 |
+
background-color: rgba(255, 255, 255, 0.1);
|
627 |
+
aspect-ratio: 16 / 9;
|
628 |
+
border-radius: 5px;
|
629 |
+
display: flex;
|
630 |
+
align-items: center;
|
631 |
+
justify-content: center;
|
632 |
+
}
|
633 |
+
|
634 |
+
.ad-actions {
|
635 |
+
display: flex;
|
636 |
+
gap: 10px;
|
637 |
+
margin-top: 20px;
|
638 |
+
}
|
639 |
+
|
640 |
+
/* === Error Page === */
|
641 |
+
.error-container {
|
642 |
+
background-color: var(--card-bg);
|
643 |
+
border-radius: 10px;
|
644 |
+
padding: 30px;
|
645 |
+
}
|
646 |
+
|
647 |
+
.error-header {
|
648 |
+
display: flex;
|
649 |
+
align-items: center;
|
650 |
+
margin-bottom: 20px;
|
651 |
+
}
|
652 |
+
|
653 |
+
.error-icon {
|
654 |
+
font-size: 3rem;
|
655 |
+
color: var(--error-color);
|
656 |
+
margin-right: 20px;
|
657 |
+
}
|
658 |
+
|
659 |
+
.error-code {
|
660 |
+
font-size: 0.9rem;
|
661 |
+
opacity: 0.7;
|
662 |
+
}
|
663 |
+
|
664 |
+
.error-message {
|
665 |
+
background-color: rgba(244, 67, 54, 0.1);
|
666 |
+
border-left: 4px solid var(--error-color);
|
667 |
+
padding: 15px;
|
668 |
+
margin-bottom: 20px;
|
669 |
+
}
|
670 |
+
|
671 |
+
.error-suggestions {
|
672 |
+
margin-bottom: 20px;
|
673 |
+
}
|
674 |
+
|
675 |
+
.error-suggestions ul {
|
676 |
+
margin-left: 20px;
|
677 |
+
}
|
678 |
+
|
679 |
+
.error-actions {
|
680 |
+
display: flex;
|
681 |
+
gap: 10px;
|
682 |
+
}
|
683 |
+
|
684 |
+
/* === Alerts === */
|
685 |
+
.alert {
|
686 |
+
padding: 15px;
|
687 |
+
border-radius: 5px;
|
688 |
+
margin-bottom: 20px;
|
689 |
+
}
|
690 |
+
|
691 |
+
.alert-success {
|
692 |
+
background-color: rgba(76, 175, 80, 0.1);
|
693 |
+
border-left: 4px solid var(--success-color);
|
694 |
+
color: var(--success-color);
|
695 |
+
}
|
696 |
+
|
697 |
+
.alert-warning {
|
698 |
+
background-color: rgba(255, 193, 7, 0.1);
|
699 |
+
border-left: 4px solid var(--warning-color);
|
700 |
+
color: var(--warning-color);
|
701 |
+
}
|
702 |
+
|
703 |
+
.alert-error {
|
704 |
+
background-color: rgba(244, 67, 54, 0.1);
|
705 |
+
border-left: 4px solid var(--error-color);
|
706 |
+
color: var(--error-color);
|
707 |
+
}
|
708 |
+
|
709 |
+
/* === Responsive Design === */
|
710 |
+
@media (max-width: 768px) {
|
711 |
+
.app-container {
|
712 |
+
flex-direction: column;
|
713 |
+
}
|
714 |
+
|
715 |
+
.sidebar {
|
716 |
+
width: 100%;
|
717 |
+
height: auto;
|
718 |
+
position: relative;
|
719 |
+
}
|
720 |
+
|
721 |
+
.content {
|
722 |
+
margin-left: 0;
|
723 |
+
width: 100%;
|
724 |
+
}
|
725 |
+
|
726 |
+
.tool-content {
|
727 |
+
grid-template-columns: 1fr;
|
728 |
+
}
|
729 |
+
|
730 |
+
.tools-grid {
|
731 |
+
grid-template-columns: 1fr;
|
732 |
+
}
|
733 |
+
|
734 |
+
.ad-info {
|
735 |
+
grid-template-columns: 1fr;
|
736 |
+
}
|
737 |
+
}
|
static/js/main.js
ADDED
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/**
|
2 |
+
* MegicAI - Main JavaScript
|
3 |
+
* Common functionality used across the application
|
4 |
+
*/
|
5 |
+
|
6 |
+
// Helper function to format numbers with commas
|
7 |
+
function formatNumber(num) {
|
8 |
+
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
9 |
+
}
|
10 |
+
|
11 |
+
// Update credit display with animation
|
12 |
+
function updateCredits(newValue, animate = true) {
|
13 |
+
const creditDisplay = document.querySelector('.credit-value');
|
14 |
+
if (!creditDisplay) return;
|
15 |
+
|
16 |
+
const currentValue = parseInt(creditDisplay.textContent.replace(/,/g, ''), 10);
|
17 |
+
|
18 |
+
if (animate && !isNaN(currentValue)) {
|
19 |
+
const diff = newValue - currentValue;
|
20 |
+
const duration = 1000; // 1 second animation
|
21 |
+
const startTime = performance.now();
|
22 |
+
|
23 |
+
function updateCounter(timestamp) {
|
24 |
+
const elapsed = timestamp - startTime;
|
25 |
+
const progress = Math.min(elapsed / duration, 1);
|
26 |
+
const currentCount = Math.floor(currentValue + (diff * progress));
|
27 |
+
|
28 |
+
creditDisplay.textContent = formatNumber(currentCount);
|
29 |
+
|
30 |
+
if (progress < 1) {
|
31 |
+
requestAnimationFrame(updateCounter);
|
32 |
+
} else {
|
33 |
+
creditDisplay.textContent = formatNumber(newValue);
|
34 |
+
}
|
35 |
+
}
|
36 |
+
|
37 |
+
requestAnimationFrame(updateCounter);
|
38 |
+
} else {
|
39 |
+
creditDisplay.textContent = formatNumber(newValue);
|
40 |
+
}
|
41 |
+
}
|
42 |
+
|
43 |
+
// Copy text to clipboard
|
44 |
+
function copyToClipboard(text, successCallback = null) {
|
45 |
+
navigator.clipboard.writeText(text)
|
46 |
+
.then(() => {
|
47 |
+
if (successCallback) successCallback();
|
48 |
+
})
|
49 |
+
.catch(err => {
|
50 |
+
console.error('Failed to copy text: ', err);
|
51 |
+
});
|
52 |
+
}
|
53 |
+
|
54 |
+
// Toggle visibility of an element
|
55 |
+
function toggleVisibility(elementId) {
|
56 |
+
const element = document.getElementById(elementId);
|
57 |
+
if (element) {
|
58 |
+
element.style.display = element.style.display === 'none' ? 'block' : 'none';
|
59 |
+
}
|
60 |
+
}
|
61 |
+
|
62 |
+
// Add suggestions to a textarea
|
63 |
+
function addSuggestionToPrompt(suggestion, elementId) {
|
64 |
+
const textarea = document.getElementById(elementId);
|
65 |
+
if (textarea) {
|
66 |
+
const currentText = textarea.value;
|
67 |
+
textarea.value = currentText ? `${currentText}\n${suggestion}` : suggestion;
|
68 |
+
textarea.focus();
|
69 |
+
}
|
70 |
+
}
|
71 |
+
|
72 |
+
// Form validation
|
73 |
+
function validateForm(formId, errorElementId = null) {
|
74 |
+
const form = document.getElementById(formId);
|
75 |
+
if (!form) return true;
|
76 |
+
|
77 |
+
let isValid = true;
|
78 |
+
const requiredInputs = form.querySelectorAll('[required]');
|
79 |
+
|
80 |
+
// Clear previous error messages
|
81 |
+
form.querySelectorAll('.field-error').forEach(el => el.remove());
|
82 |
+
|
83 |
+
requiredInputs.forEach(input => {
|
84 |
+
if (!input.value.trim()) {
|
85 |
+
isValid = false;
|
86 |
+
|
87 |
+
// Create error message
|
88 |
+
const errorMsg = document.createElement('div');
|
89 |
+
errorMsg.className = 'field-error';
|
90 |
+
errorMsg.textContent = 'This field is required';
|
91 |
+
input.parentNode.appendChild(errorMsg);
|
92 |
+
|
93 |
+
// Add error style to input
|
94 |
+
input.classList.add('input-error');
|
95 |
+
} else {
|
96 |
+
input.classList.remove('input-error');
|
97 |
+
}
|
98 |
+
});
|
99 |
+
|
100 |
+
// If there's a global error element, update it
|
101 |
+
if (!isValid && errorElementId) {
|
102 |
+
const errorElement = document.getElementById(errorElementId);
|
103 |
+
if (errorElement) {
|
104 |
+
errorElement.textContent = 'Please fill in all required fields';
|
105 |
+
errorElement.style.display = 'block';
|
106 |
+
}
|
107 |
+
}
|
108 |
+
|
109 |
+
return isValid;
|
110 |
+
}
|
111 |
+
|
112 |
+
// Handle form submission with AJAX
|
113 |
+
function submitFormAsync(formId, successCallback, errorCallback) {
|
114 |
+
const form = document.getElementById(formId);
|
115 |
+
if (!form) return;
|
116 |
+
|
117 |
+
form.addEventListener('submit', function(event) {
|
118 |
+
event.preventDefault();
|
119 |
+
|
120 |
+
if (!validateForm(formId)) return;
|
121 |
+
|
122 |
+
const formData = new FormData(form);
|
123 |
+
const submitButton = form.querySelector('button[type="submit"]');
|
124 |
+
|
125 |
+
if (submitButton) {
|
126 |
+
const originalText = submitButton.innerHTML;
|
127 |
+
submitButton.disabled = true;
|
128 |
+
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...';
|
129 |
+
}
|
130 |
+
|
131 |
+
fetch(form.action, {
|
132 |
+
method: form.method,
|
133 |
+
body: formData
|
134 |
+
})
|
135 |
+
.then(response => {
|
136 |
+
// Check if the response is JSON or HTML
|
137 |
+
const contentType = response.headers.get('content-type');
|
138 |
+
if (!response.ok) {
|
139 |
+
if (contentType && contentType.includes('application/json')) {
|
140 |
+
return response.json().then(data => {
|
141 |
+
throw new Error(data.message || 'An error occurred');
|
142 |
+
});
|
143 |
+
} else {
|
144 |
+
// For non-JSON errors, just show the status text instead of trying to parse JSON
|
145 |
+
throw new Error('Server error occurred: ' + response.statusText);
|
146 |
+
}
|
147 |
+
}
|
148 |
+
|
149 |
+
// If response is HTML, handle it as a redirect
|
150 |
+
if (contentType && contentType.includes('text/html')) {
|
151 |
+
// This is an HTML response, likely a result page - redirect to it
|
152 |
+
window.location.href = response.url;
|
153 |
+
return { success: true, redirected: true };
|
154 |
+
}
|
155 |
+
|
156 |
+
// Check if it's a redirect (e.g., 302, 303)
|
157 |
+
if (response.redirected) {
|
158 |
+
window.location.href = response.url;
|
159 |
+
return { success: true, redirected: true };
|
160 |
+
}
|
161 |
+
|
162 |
+
// Otherwise process as JSON
|
163 |
+
if (contentType && contentType.includes('application/json')) {
|
164 |
+
return response.json();
|
165 |
+
} else {
|
166 |
+
// If it's not JSON and not HTML with a redirect, handle it as a success
|
167 |
+
return { success: true, message: "Operation completed successfully" };
|
168 |
+
}
|
169 |
+
})
|
170 |
+
.then(data => {
|
171 |
+
if (successCallback) successCallback(data);
|
172 |
+
})
|
173 |
+
.catch(error => {
|
174 |
+
console.error("Error during form submission:", error);
|
175 |
+
if (errorCallback) errorCallback(error.message);
|
176 |
+
})
|
177 |
+
.finally(() => {
|
178 |
+
if (submitButton) {
|
179 |
+
submitButton.disabled = false;
|
180 |
+
submitButton.innerHTML = originalText;
|
181 |
+
}
|
182 |
+
});
|
183 |
+
});
|
184 |
+
}
|
185 |
+
|
186 |
+
// Document ready event
|
187 |
+
document.addEventListener('DOMContentLoaded', function() {
|
188 |
+
console.log('MegicAI application initialized');
|
189 |
+
|
190 |
+
// Initialize any forms with async submission
|
191 |
+
const asyncForms = document.querySelectorAll('[data-async-submit]');
|
192 |
+
asyncForms.forEach(form => {
|
193 |
+
const formId = form.id;
|
194 |
+
const successTarget = form.getAttribute('data-success-target');
|
195 |
+
const errorTarget = form.getAttribute('data-error-target');
|
196 |
+
|
197 |
+
// Check if the form is for HTML-based operations like process-request
|
198 |
+
const actionUrl = form.getAttribute('action');
|
199 |
+
if (actionUrl && (actionUrl.includes('/process-request') || actionUrl.includes('/watch-ad'))) {
|
200 |
+
// These endpoints return HTML, not JSON - use regular form submission
|
201 |
+
console.log('Form will use direct submission:', actionUrl);
|
202 |
+
form.removeAttribute('data-async-submit');
|
203 |
+
|
204 |
+
// Add regular submit handler with validation
|
205 |
+
form.addEventListener('submit', function(event) {
|
206 |
+
if (!validateForm(formId)) {
|
207 |
+
event.preventDefault();
|
208 |
+
return false;
|
209 |
+
}
|
210 |
+
|
211 |
+
const submitButton = form.querySelector('button[type="submit"]');
|
212 |
+
if (submitButton) {
|
213 |
+
// Add loading indicator
|
214 |
+
const originalText = submitButton.innerHTML;
|
215 |
+
submitButton.disabled = true;
|
216 |
+
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...';
|
217 |
+
|
218 |
+
// Make sure the form submits normally for HTML responses
|
219 |
+
setTimeout(() => {
|
220 |
+
if (submitButton.disabled) {
|
221 |
+
// Re-enable after a timeout just in case
|
222 |
+
submitButton.disabled = false;
|
223 |
+
submitButton.innerHTML = originalText;
|
224 |
+
}
|
225 |
+
}, 10000); // 10 second timeout
|
226 |
+
}
|
227 |
+
|
228 |
+
return true;
|
229 |
+
});
|
230 |
+
} else {
|
231 |
+
// Use async submission for JSON-based API endpoints
|
232 |
+
submitFormAsync(
|
233 |
+
formId,
|
234 |
+
data => {
|
235 |
+
if (successTarget) {
|
236 |
+
const targetElement = document.getElementById(successTarget);
|
237 |
+
if (targetElement) {
|
238 |
+
targetElement.textContent = data.message || 'Success!';
|
239 |
+
targetElement.style.display = 'block';
|
240 |
+
}
|
241 |
+
}
|
242 |
+
|
243 |
+
// If a redirect URL is provided in the response
|
244 |
+
if (data.redirect) {
|
245 |
+
window.location.href = data.redirect;
|
246 |
+
}
|
247 |
+
},
|
248 |
+
error => {
|
249 |
+
if (errorTarget) {
|
250 |
+
const targetElement = document.getElementById(errorTarget);
|
251 |
+
if (targetElement) {
|
252 |
+
targetElement.textContent = error;
|
253 |
+
targetElement.style.display = 'block';
|
254 |
+
}
|
255 |
+
}
|
256 |
+
}
|
257 |
+
);
|
258 |
+
}
|
259 |
+
});
|
260 |
+
});
|
templates/ad.html
ADDED
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>{{ app_name }} - Watch Ad</title>
|
7 |
+
<link rel="stylesheet" href="{{ url_for('static', path='/css/styles.css') }}">
|
8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
<div class="app-container">
|
12 |
+
<!-- Sidebar -->
|
13 |
+
<div class="sidebar">
|
14 |
+
<div class="sidebar-header">
|
15 |
+
<h1>{{ app_name }}</h1>
|
16 |
+
<div class="sidebar-subtitle">AI Toolkit</div>
|
17 |
+
</div>
|
18 |
+
|
19 |
+
<!-- Credits Section -->
|
20 |
+
<div class="sidebar-section">
|
21 |
+
<div class="credit-display">
|
22 |
+
<div class="credit-icon">
|
23 |
+
<i class="fas fa-coins"></i>
|
24 |
+
</div>
|
25 |
+
<div>
|
26 |
+
<div class="credit-label">Your Credits</div>
|
27 |
+
<div class="credit-value">{{ user_credits }}</div>
|
28 |
+
</div>
|
29 |
+
</div>
|
30 |
+
</div>
|
31 |
+
|
32 |
+
<!-- Navigation -->
|
33 |
+
<div class="sidebar-section">
|
34 |
+
<div class="sidebar-heading">Navigation</div>
|
35 |
+
<a href="/" class="nav-item">
|
36 |
+
<div class="nav-icon"><i class="fas fa-home"></i></div>
|
37 |
+
<div>Home</div>
|
38 |
+
</a>
|
39 |
+
<a href="#" class="nav-item">
|
40 |
+
<div class="nav-icon"><i class="fas fa-history"></i></div>
|
41 |
+
<div>History</div>
|
42 |
+
</a>
|
43 |
+
<a href="#" class="nav-item">
|
44 |
+
<div class="nav-icon"><i class="fas fa-coins"></i></div>
|
45 |
+
<div>Buy Credits</div>
|
46 |
+
</a>
|
47 |
+
</div>
|
48 |
+
|
49 |
+
<!-- Tools -->
|
50 |
+
<div class="sidebar-section">
|
51 |
+
<div class="sidebar-heading">Tools</div>
|
52 |
+
{% for tool in tools %}
|
53 |
+
<a href="/tool/{{ tool.id }}" class="nav-item">
|
54 |
+
<div class="nav-icon"><i class="{{ tool.icon }}"></i></div>
|
55 |
+
<div>{{ tool.name }}</div>
|
56 |
+
</a>
|
57 |
+
{% endfor %}
|
58 |
+
</div>
|
59 |
+
|
60 |
+
<div class="sidebar-footer">
|
61 |
+
© {{ app_name }} 2023
|
62 |
+
</div>
|
63 |
+
</div>
|
64 |
+
|
65 |
+
<!-- Content -->
|
66 |
+
<div class="content">
|
67 |
+
<div class="content-header">
|
68 |
+
<h1>Watch an Ad to Earn Credits</h1>
|
69 |
+
<p>Watch this ad to gain free credits for using AI tools.</p>
|
70 |
+
</div>
|
71 |
+
|
72 |
+
<div class="ad-content">
|
73 |
+
<div class="ad-header">
|
74 |
+
<h2>Ad Information</h2>
|
75 |
+
</div>
|
76 |
+
|
77 |
+
<div class="ad-info">
|
78 |
+
<div class="ad-reward">
|
79 |
+
<div class="reward-icon">
|
80 |
+
<i class="fas fa-coins"></i>
|
81 |
+
</div>
|
82 |
+
<div>
|
83 |
+
<h3>Reward</h3>
|
84 |
+
<p>{{ ad_reward }} credits</p>
|
85 |
+
</div>
|
86 |
+
</div>
|
87 |
+
|
88 |
+
<div class="ad-duration">
|
89 |
+
<div class="duration-icon">
|
90 |
+
<i class="fas fa-clock"></i>
|
91 |
+
</div>
|
92 |
+
<div>
|
93 |
+
<h3>Duration</h3>
|
94 |
+
<p>{{ ad_duration }} seconds</p>
|
95 |
+
</div>
|
96 |
+
</div>
|
97 |
+
</div>
|
98 |
+
|
99 |
+
<div class="ad-player">
|
100 |
+
<div class="ad-container">
|
101 |
+
<div class="mock-ad">
|
102 |
+
<div class="mock-ad-content">
|
103 |
+
<h2>Advertisement</h2>
|
104 |
+
<p>This is a sample advertisement. In a real implementation, this would be an actual ad from an ad network.</p>
|
105 |
+
<div class="mock-ad-image">
|
106 |
+
<i class="fas fa-image fa-4x"></i>
|
107 |
+
</div>
|
108 |
+
</div>
|
109 |
+
</div>
|
110 |
+
</div>
|
111 |
+
|
112 |
+
<div class="ad-timer-container">
|
113 |
+
<div class="ad-timer">
|
114 |
+
<span id="ad-countdown">{{ ad_duration }}</span> seconds remaining
|
115 |
+
</div>
|
116 |
+
</div>
|
117 |
+
</div>
|
118 |
+
|
119 |
+
<form action="/complete-ad" method="post">
|
120 |
+
<input type="hidden" name="tool_id" value="{{ tool_id }}">
|
121 |
+
<input type="hidden" name="prompt" value="{{ prompt }}">
|
122 |
+
<input type="hidden" name="provider" value="{{ provider }}">
|
123 |
+
<input type="hidden" name="model" value="{{ model }}">
|
124 |
+
|
125 |
+
<div class="ad-actions">
|
126 |
+
<button id="complete-ad-btn" class="btn btn-primary" disabled>
|
127 |
+
Please wait <span id="btn-countdown">{{ ad_duration }}</span>s
|
128 |
+
</button>
|
129 |
+
<a href="/" class="btn btn-secondary">Cancel</a>
|
130 |
+
</div>
|
131 |
+
</form>
|
132 |
+
</div>
|
133 |
+
</div>
|
134 |
+
</div>
|
135 |
+
|
136 |
+
<script>
|
137 |
+
document.addEventListener('DOMContentLoaded', function() {
|
138 |
+
const adDuration = {{ ad_duration }};
|
139 |
+
let timeRemaining = adDuration;
|
140 |
+
const countdownElement = document.getElementById('ad-countdown');
|
141 |
+
const btnCountdownElement = document.getElementById('btn-countdown');
|
142 |
+
const completeAdBtn = document.getElementById('complete-ad-btn');
|
143 |
+
|
144 |
+
const countdownInterval = setInterval(function() {
|
145 |
+
timeRemaining -= 1;
|
146 |
+
|
147 |
+
if (timeRemaining <= 0) {
|
148 |
+
clearInterval(countdownInterval);
|
149 |
+
countdownElement.textContent = '0';
|
150 |
+
completeAdBtn.disabled = false;
|
151 |
+
completeAdBtn.innerHTML = 'Complete and Get Credits <i class="fas fa-check"></i>';
|
152 |
+
} else {
|
153 |
+
countdownElement.textContent = timeRemaining;
|
154 |
+
btnCountdownElement.textContent = timeRemaining;
|
155 |
+
}
|
156 |
+
}, 1000);
|
157 |
+
});
|
158 |
+
</script>
|
159 |
+
</body>
|
160 |
+
</html>
|
templates/ad_reward.html
ADDED
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends "base.html" %}
|
2 |
+
|
3 |
+
{% block title %}Watch Ad & Earn Credits{% endblock %}
|
4 |
+
|
5 |
+
{% block content %}
|
6 |
+
<div class="max-w-4xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
|
7 |
+
<div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
|
8 |
+
<div class="px-6 py-5 border-b border-gray-200 dark:border-gray-700">
|
9 |
+
<h1 class="text-xl font-semibold text-gray-900 dark:text-white">
|
10 |
+
Watch Ad & Earn Credits
|
11 |
+
</h1>
|
12 |
+
</div>
|
13 |
+
|
14 |
+
<div class="px-6 py-4">
|
15 |
+
<div class="flex flex-col lg:flex-row gap-6">
|
16 |
+
<div class="w-full lg:w-2/3">
|
17 |
+
<div class="bg-gray-100 dark:bg-gray-700 rounded-lg p-4 mb-6">
|
18 |
+
<h2 class="text-lg font-medium text-gray-900 dark:text-white mb-2">How it works</h2>
|
19 |
+
<p class="text-gray-600 dark:text-gray-300 mb-4">
|
20 |
+
Watch a short advertisement to earn credits that you can use to generate content with our AI tools.
|
21 |
+
</p>
|
22 |
+
<ul class="list-disc list-inside text-gray-600 dark:text-gray-300 mb-4">
|
23 |
+
<li>Complete the ad to earn {{ tool.cost }} credits</li>
|
24 |
+
<li>You will be redirected back to the tool automatically</li>
|
25 |
+
<li>Your credits will be available immediately</li>
|
26 |
+
</ul>
|
27 |
+
</div>
|
28 |
+
|
29 |
+
<!-- Ad Container -->
|
30 |
+
<div id="ad-container" class="bg-gray-100 dark:bg-gray-700 rounded-lg overflow-hidden relative">
|
31 |
+
<div id="ad-placeholder" class="flex flex-col items-center justify-center p-10 text-center">
|
32 |
+
<div class="mb-4">
|
33 |
+
<svg class="w-16 h-16 text-gray-400 dark:text-gray-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
34 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>
|
35 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
36 |
+
</svg>
|
37 |
+
</div>
|
38 |
+
<p class="text-gray-600 dark:text-gray-300 mb-6">Loading advertisement...</p>
|
39 |
+
<button id="start-ad-btn" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
40 |
+
Start Watching
|
41 |
+
</button>
|
42 |
+
</div>
|
43 |
+
|
44 |
+
<div id="ad-content" class="hidden">
|
45 |
+
<!-- This is where the actual ad will be displayed -->
|
46 |
+
{% if reward_ad and reward_ad.success %}
|
47 |
+
{{ reward_ad.ad_code|safe }}
|
48 |
+
{% else %}
|
49 |
+
<div class="p-6 text-center">
|
50 |
+
<p class="text-red-500">Sorry, no ads are available at the moment. Please try again later.</p>
|
51 |
+
</div>
|
52 |
+
{% endif %}
|
53 |
+
</div>
|
54 |
+
|
55 |
+
<div id="ad-completed" class="hidden flex flex-col items-center justify-center p-10 text-center">
|
56 |
+
<div class="mb-4">
|
57 |
+
<svg class="w-16 h-16 text-green-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
58 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
59 |
+
</svg>
|
60 |
+
</div>
|
61 |
+
<h3 class="text-xl font-medium text-gray-900 dark:text-white mb-2">Thank You!</h3>
|
62 |
+
<p class="text-gray-600 dark:text-gray-300 mb-6">
|
63 |
+
You've earned <span class="font-bold text-green-500">{{ tool.cost }} credits</span>
|
64 |
+
</p>
|
65 |
+
<a href="{{ return_url }}" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
66 |
+
Return to Tool
|
67 |
+
</a>
|
68 |
+
</div>
|
69 |
+
</div>
|
70 |
+
</div>
|
71 |
+
|
72 |
+
<div class="w-full lg:w-1/3">
|
73 |
+
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 mb-6">
|
74 |
+
<h2 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Your Balance</h2>
|
75 |
+
<div class="flex items-center justify-between">
|
76 |
+
<div class="flex items-center">
|
77 |
+
<svg class="w-6 h-6 text-yellow-500 mr-2" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
78 |
+
<path d="M8.433 7.418c.155-.103.346-.196.567-.267v1.698a2.305 2.305 0 01-.567-.267C8.07 8.34 8 8.114 8 8c0-.114.07-.34.433-.582zM11 12.849v-1.698c.22.071.412.164.567.267.364.243.433.468.433.582 0 .114-.07.34-.433.582a2.305 2.305 0 01-.567.267z"></path>
|
79 |
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-13a1 1 0 10-2 0v.092a4.535 4.535 0 00-1.676.662C6.602 6.234 6 7.009 6 8c0 .99.602 1.765 1.324 2.246.48.32 1.054.545 1.676.662v1.941c-.391-.127-.68-.317-.843-.504a1 1 0 10-1.51 1.31c.562.649 1.413 1.076 2.353 1.253V15a1 1 0 102 0v-.092a4.535 4.535 0 001.676-.662C13.398 13.766 14 12.991 14 12c0-.99-.602-1.765-1.324-2.246A4.535 4.535 0 0011 9.092V7.151c.391.127.68.317.843.504a1 1 0 101.511-1.31c-.563-.649-1.413-1.076-2.354-1.253V5z" clip-rule="evenodd"></path>
|
80 |
+
</svg>
|
81 |
+
<span class="text-gray-900 dark:text-white font-semibold" id="current-credits">{{ user_credits }}</span>
|
82 |
+
</div>
|
83 |
+
<div>
|
84 |
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-800 dark:text-blue-100">
|
85 |
+
Credits
|
86 |
+
</span>
|
87 |
+
</div>
|
88 |
+
</div>
|
89 |
+
</div>
|
90 |
+
|
91 |
+
<!-- Tool info -->
|
92 |
+
<div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
93 |
+
<h2 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Tool Information</h2>
|
94 |
+
<div class="flex items-center mb-2">
|
95 |
+
<span class="text-gray-600 dark:text-gray-300">{{ tool.name }}</span>
|
96 |
+
</div>
|
97 |
+
<div class="flex items-center mb-2">
|
98 |
+
<span class="text-gray-600 dark:text-gray-300 mr-2">Cost:</span>
|
99 |
+
<span class="font-semibold text-gray-900 dark:text-white">{{ tool.cost }} credits</span>
|
100 |
+
</div>
|
101 |
+
<p class="text-gray-600 dark:text-gray-300 text-sm">
|
102 |
+
{{ tool.description }}
|
103 |
+
</p>
|
104 |
+
</div>
|
105 |
+
|
106 |
+
<!-- Sidebar ad -->
|
107 |
+
<div class="mt-6">
|
108 |
+
{% if sidebar_ad and sidebar_ad.success %}
|
109 |
+
{{ sidebar_ad.ad_code|safe }}
|
110 |
+
{% endif %}
|
111 |
+
</div>
|
112 |
+
</div>
|
113 |
+
</div>
|
114 |
+
</div>
|
115 |
+
</div>
|
116 |
+
</div>
|
117 |
+
{% endblock %}
|
118 |
+
|
119 |
+
{% block scripts %}
|
120 |
+
<script>
|
121 |
+
// Set up variables from Jinja templates
|
122 |
+
var toolId = "{{ tool.id }}";
|
123 |
+
var returnUrl = "{{ return_url }}";
|
124 |
+
{% if reward_ad and reward_ad.success %}
|
125 |
+
var hasRewardAd = true;
|
126 |
+
var impressionId = "{{ reward_ad.impression_id }}";
|
127 |
+
{% else %}
|
128 |
+
var hasRewardAd = false;
|
129 |
+
var impressionId = null;
|
130 |
+
{% endif %}
|
131 |
+
|
132 |
+
// Get DOM elements
|
133 |
+
var adContainer = document.getElementById('ad-container');
|
134 |
+
var adPlaceholder = document.getElementById('ad-placeholder');
|
135 |
+
var adContent = document.getElementById('ad-content');
|
136 |
+
var adCompleted = document.getElementById('ad-completed');
|
137 |
+
var startAdBtn = document.getElementById('start-ad-btn');
|
138 |
+
var currentCredits = document.getElementById('current-credits');
|
139 |
+
|
140 |
+
// Set up event listener for ad start button
|
141 |
+
startAdBtn.addEventListener('click', function() {
|
142 |
+
adPlaceholder.classList.add('hidden');
|
143 |
+
adContent.classList.remove('hidden');
|
144 |
+
|
145 |
+
// Simulate ad completion after a delay
|
146 |
+
var adDuration = hasRewardAd ? 5000 : 3000;
|
147 |
+
setTimeout(function() {
|
148 |
+
completeAd(impressionId);
|
149 |
+
}, adDuration);
|
150 |
+
});
|
151 |
+
|
152 |
+
// Handle ad completion
|
153 |
+
function completeAd(adImpressionId) {
|
154 |
+
// Hide ad content
|
155 |
+
adContent.classList.add('hidden');
|
156 |
+
|
157 |
+
// Show loading spinner
|
158 |
+
var loadingEl = document.createElement('div');
|
159 |
+
loadingEl.className = 'flex justify-center items-center p-10';
|
160 |
+
loadingEl.innerHTML =
|
161 |
+
'<svg class="animate-spin h-10 w-10 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">' +
|
162 |
+
'<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>' +
|
163 |
+
'<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>' +
|
164 |
+
'</svg>';
|
165 |
+
adContainer.appendChild(loadingEl);
|
166 |
+
|
167 |
+
// Send completion to server
|
168 |
+
fetch('/api/ads/complete', {
|
169 |
+
method: 'POST',
|
170 |
+
headers: {
|
171 |
+
'Content-Type': 'application/json'
|
172 |
+
},
|
173 |
+
body: JSON.stringify({
|
174 |
+
ad_type: 'reward_video',
|
175 |
+
impression_id: adImpressionId,
|
176 |
+
tool_id: toolId
|
177 |
+
})
|
178 |
+
})
|
179 |
+
.then(function(response) {
|
180 |
+
return response.json();
|
181 |
+
})
|
182 |
+
.then(function(data) {
|
183 |
+
// Remove loading spinner
|
184 |
+
adContainer.removeChild(loadingEl);
|
185 |
+
|
186 |
+
if (data.success) {
|
187 |
+
// Update credits display
|
188 |
+
if (currentCredits) {
|
189 |
+
currentCredits.textContent = data.current_credits;
|
190 |
+
}
|
191 |
+
|
192 |
+
// Also update header credits if available
|
193 |
+
var headerCredits = document.getElementById('user-credits');
|
194 |
+
if (headerCredits) {
|
195 |
+
headerCredits.textContent = data.current_credits;
|
196 |
+
}
|
197 |
+
|
198 |
+
// Show completion message
|
199 |
+
adCompleted.classList.remove('hidden');
|
200 |
+
|
201 |
+
// Show success toast
|
202 |
+
showToast('You earned ' + data.credits_earned + ' credits!', 'success');
|
203 |
+
|
204 |
+
// Redirect after delay if auto-redirect is enabled
|
205 |
+
setTimeout(function() {
|
206 |
+
window.location.href = returnUrl;
|
207 |
+
}, 3000);
|
208 |
+
} else {
|
209 |
+
// Show error message
|
210 |
+
showToast('Error: ' + data.error, 'error');
|
211 |
+
adPlaceholder.classList.remove('hidden');
|
212 |
+
startAdBtn.textContent = 'Try Again';
|
213 |
+
}
|
214 |
+
})
|
215 |
+
.catch(function(error) {
|
216 |
+
console.error('Error:', error);
|
217 |
+
// Remove loading spinner
|
218 |
+
adContainer.removeChild(loadingEl);
|
219 |
+
// Show error message
|
220 |
+
showToast('Network error, please try again', 'error');
|
221 |
+
adPlaceholder.classList.remove('hidden');
|
222 |
+
startAdBtn.textContent = 'Try Again';
|
223 |
+
});
|
224 |
+
}
|
225 |
+
</script>
|
226 |
+
{% endblock %}
|
templates/admin_dashboard.html
ADDED
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends "base.html" %}
|
2 |
+
|
3 |
+
{% block title %}Admin Dashboard{% endblock %}
|
4 |
+
|
5 |
+
{% block content %}
|
6 |
+
<div class="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
|
7 |
+
<div class="mb-8">
|
8 |
+
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Admin Dashboard</h1>
|
9 |
+
<p class="text-gray-600 dark:text-gray-400">Manage users and credits</p>
|
10 |
+
</div>
|
11 |
+
|
12 |
+
<div class="mb-8 grid grid-cols-1 md:grid-cols-3 gap-6">
|
13 |
+
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
|
14 |
+
<div class="px-4 py-5 sm:px-6">
|
15 |
+
<h3 class="text-lg font-medium text-gray-900 dark:text-white">User Statistics</h3>
|
16 |
+
</div>
|
17 |
+
<div class="px-4 py-5 sm:p-6">
|
18 |
+
<dl>
|
19 |
+
<div class="mb-4">
|
20 |
+
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Users</dt>
|
21 |
+
<dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white">{{ total_users }}</dd>
|
22 |
+
</div>
|
23 |
+
<div class="mb-4">
|
24 |
+
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Admin Users</dt>
|
25 |
+
<dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white">{{ admin_count }}</dd>
|
26 |
+
</div>
|
27 |
+
<div>
|
28 |
+
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Total Credits Issued</dt>
|
29 |
+
<dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white">{{ total_credits }}</dd>
|
30 |
+
</div>
|
31 |
+
</dl>
|
32 |
+
</div>
|
33 |
+
</div>
|
34 |
+
|
35 |
+
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg md:col-span-2">
|
36 |
+
<div class="px-4 py-5 sm:px-6">
|
37 |
+
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Quick Actions</h3>
|
38 |
+
</div>
|
39 |
+
<div class="px-4 py-5 sm:p-6">
|
40 |
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
41 |
+
<button id="addCreditsAllBtn" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
42 |
+
Add Credits to All Users
|
43 |
+
</button>
|
44 |
+
<button id="addNewUserBtn" class="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2">
|
45 |
+
Add New User
|
46 |
+
</button>
|
47 |
+
<button id="clearSessionsBtn" class="bg-yellow-600 text-white px-4 py-2 rounded-md hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:ring-offset-2">
|
48 |
+
Clear Expired Sessions
|
49 |
+
</button>
|
50 |
+
<button id="resetCreditsBtn" class="bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2">
|
51 |
+
Reset All Credits
|
52 |
+
</button>
|
53 |
+
</div>
|
54 |
+
</div>
|
55 |
+
</div>
|
56 |
+
</div>
|
57 |
+
|
58 |
+
<div class="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-md">
|
59 |
+
<div class="px-4 py-5 sm:px-6 flex justify-between items-center">
|
60 |
+
<h3 class="text-lg font-medium text-gray-900 dark:text-white">User Management</h3>
|
61 |
+
<div class="relative">
|
62 |
+
<input type="text" id="userSearch" placeholder="Search users..." class="border border-gray-300 dark:border-gray-600 rounded-md px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white">
|
63 |
+
</div>
|
64 |
+
</div>
|
65 |
+
<div class="overflow-x-auto">
|
66 |
+
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
67 |
+
<thead class="bg-gray-50 dark:bg-gray-700">
|
68 |
+
<tr>
|
69 |
+
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
70 |
+
User
|
71 |
+
</th>
|
72 |
+
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
73 |
+
Role
|
74 |
+
</th>
|
75 |
+
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
76 |
+
Credits
|
77 |
+
</th>
|
78 |
+
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
|
79 |
+
Actions
|
80 |
+
</th>
|
81 |
+
</tr>
|
82 |
+
</thead>
|
83 |
+
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700" id="userTable">
|
84 |
+
{% for user in users %}
|
85 |
+
<tr class="user-row" data-user-id="{{ user.id }}">
|
86 |
+
<td class="px-6 py-4 whitespace-nowrap">
|
87 |
+
<div>
|
88 |
+
<div class="text-sm font-medium text-gray-900 dark:text-white">
|
89 |
+
{{ user.username }}
|
90 |
+
</div>
|
91 |
+
<div class="text-sm text-gray-500 dark:text-gray-400">
|
92 |
+
{{ user.email }}
|
93 |
+
</div>
|
94 |
+
</div>
|
95 |
+
</td>
|
96 |
+
<td class="px-6 py-4 whitespace-nowrap">
|
97 |
+
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {% if user.role == 'admin' %}bg-purple-100 text-purple-800 dark:bg-purple-800 dark:text-purple-100{% else %}bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100{% endif %}">
|
98 |
+
{{ user.role }}
|
99 |
+
</span>
|
100 |
+
</td>
|
101 |
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
102 |
+
<span class="credits-display">{{ user.credits }}</span>
|
103 |
+
<div class="edit-credits hidden">
|
104 |
+
<input type="number" class="credits-input w-20 border border-gray-300 dark:border-gray-600 rounded p-1 dark:bg-gray-700 dark:text-white" value="{{ user.credits }}">
|
105 |
+
<button class="save-credits ml-2 text-xs bg-blue-600 text-white px-2 py-1 rounded">Save</button>
|
106 |
+
</div>
|
107 |
+
</td>
|
108 |
+
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
109 |
+
<button class="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 edit-credits-btn">
|
110 |
+
Edit Credits
|
111 |
+
</button>
|
112 |
+
<span class="mx-2 text-gray-300 dark:text-gray-600">|</span>
|
113 |
+
<button class="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300 delete-user-btn" {% if user.role == 'admin' %}disabled{% endif %}>
|
114 |
+
{% if user.role == 'admin' %}Admin{% else %}Delete{% endif %}
|
115 |
+
</button>
|
116 |
+
</td>
|
117 |
+
</tr>
|
118 |
+
{% endfor %}
|
119 |
+
</tbody>
|
120 |
+
</table>
|
121 |
+
</div>
|
122 |
+
</div>
|
123 |
+
</div>
|
124 |
+
|
125 |
+
<!-- Modal for adding credits to all users -->
|
126 |
+
<div id="addCreditsModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
127 |
+
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full mx-4">
|
128 |
+
<h3 class="text-xl font-semibold mb-4 text-gray-900 dark:text-white">Add Credits to All Users</h3>
|
129 |
+
<div class="mb-4">
|
130 |
+
<label for="creditsAmount" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Credits to add:</label>
|
131 |
+
<input type="number" id="creditsAmount" class="w-full border border-gray-300 dark:border-gray-600 rounded-md px-4 py-2 dark:bg-gray-700 dark:text-white" value="100">
|
132 |
+
</div>
|
133 |
+
<div class="flex justify-end space-x-4">
|
134 |
+
<button id="cancelAddCredits" class="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200">
|
135 |
+
Cancel
|
136 |
+
</button>
|
137 |
+
<button id="confirmAddCredits" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
138 |
+
Add Credits
|
139 |
+
</button>
|
140 |
+
</div>
|
141 |
+
</div>
|
142 |
+
</div>
|
143 |
+
{% endblock %}
|
144 |
+
|
145 |
+
{% block scripts %}
|
146 |
+
<script>
|
147 |
+
// Search functionality
|
148 |
+
document.getElementById('userSearch').addEventListener('input', function(e) {
|
149 |
+
const searchTerm = e.target.value.toLowerCase();
|
150 |
+
const rows = document.querySelectorAll('.user-row');
|
151 |
+
|
152 |
+
rows.forEach(row => {
|
153 |
+
const username = row.querySelector('.text-sm.font-medium').textContent.toLowerCase();
|
154 |
+
const email = row.querySelector('.text-sm.text-gray-500').textContent.toLowerCase();
|
155 |
+
|
156 |
+
if (username.includes(searchTerm) || email.includes(searchTerm)) {
|
157 |
+
row.style.display = '';
|
158 |
+
} else {
|
159 |
+
row.style.display = 'none';
|
160 |
+
}
|
161 |
+
});
|
162 |
+
});
|
163 |
+
|
164 |
+
// Edit credits functionality
|
165 |
+
document.querySelectorAll('.edit-credits-btn').forEach(btn => {
|
166 |
+
btn.addEventListener('click', function() {
|
167 |
+
const row = this.closest('tr');
|
168 |
+
const creditsDisplay = row.querySelector('.credits-display');
|
169 |
+
const editCredits = row.querySelector('.edit-credits');
|
170 |
+
|
171 |
+
creditsDisplay.classList.toggle('hidden');
|
172 |
+
editCredits.classList.toggle('hidden');
|
173 |
+
|
174 |
+
if (!editCredits.classList.contains('hidden')) {
|
175 |
+
editCredits.querySelector('input').focus();
|
176 |
+
}
|
177 |
+
});
|
178 |
+
});
|
179 |
+
|
180 |
+
// Save credits functionality
|
181 |
+
document.querySelectorAll('.save-credits').forEach(btn => {
|
182 |
+
btn.addEventListener('click', function() {
|
183 |
+
const row = this.closest('tr');
|
184 |
+
const userId = row.dataset.userId;
|
185 |
+
const newCredits = parseFloat(row.querySelector('.credits-input').value);
|
186 |
+
const creditsDisplay = row.querySelector('.credits-display');
|
187 |
+
const editCredits = row.querySelector('.edit-credits');
|
188 |
+
|
189 |
+
fetch('/api/admin/update-credits', {
|
190 |
+
method: 'POST',
|
191 |
+
headers: {
|
192 |
+
'Content-Type': 'application/json'
|
193 |
+
},
|
194 |
+
body: JSON.stringify({
|
195 |
+
user_id: userId,
|
196 |
+
credits: newCredits
|
197 |
+
})
|
198 |
+
})
|
199 |
+
.then(response => response.json())
|
200 |
+
.then(data => {
|
201 |
+
if (data.success) {
|
202 |
+
creditsDisplay.textContent = newCredits;
|
203 |
+
creditsDisplay.classList.toggle('hidden');
|
204 |
+
editCredits.classList.toggle('hidden');
|
205 |
+
showToast(data.message, 'success');
|
206 |
+
} else {
|
207 |
+
showToast('Error: ' + data.error, 'error');
|
208 |
+
}
|
209 |
+
})
|
210 |
+
.catch(error => {
|
211 |
+
console.error('Error:', error);
|
212 |
+
showToast('Failed to update credits', 'error');
|
213 |
+
});
|
214 |
+
});
|
215 |
+
});
|
216 |
+
|
217 |
+
// Modal functionality
|
218 |
+
const addCreditsModal = document.getElementById('addCreditsModal');
|
219 |
+
|
220 |
+
document.getElementById('addCreditsAllBtn').addEventListener('click', function() {
|
221 |
+
addCreditsModal.classList.remove('hidden');
|
222 |
+
addCreditsModal.classList.add('flex');
|
223 |
+
});
|
224 |
+
|
225 |
+
document.getElementById('cancelAddCredits').addEventListener('click', function() {
|
226 |
+
addCreditsModal.classList.add('hidden');
|
227 |
+
addCreditsModal.classList.remove('flex');
|
228 |
+
});
|
229 |
+
|
230 |
+
// Close modal when clicking outside
|
231 |
+
addCreditsModal.addEventListener('click', function(e) {
|
232 |
+
if (e.target === addCreditsModal) {
|
233 |
+
addCreditsModal.classList.add('hidden');
|
234 |
+
addCreditsModal.classList.remove('flex');
|
235 |
+
}
|
236 |
+
});
|
237 |
+
</script>
|
238 |
+
{% endblock %}
|
templates/base.html
ADDED
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>{{ app_name }} - {% block title %}{% endblock %}</title>
|
7 |
+
|
8 |
+
<!-- Tailwind CSS -->
|
9 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
10 |
+
|
11 |
+
<!-- Custom CSS -->
|
12 |
+
<style>
|
13 |
+
/* Dark mode styles */
|
14 |
+
@media (prefers-color-scheme: dark) {
|
15 |
+
body {
|
16 |
+
background-color: #1a1a1a;
|
17 |
+
color: #ffffff;
|
18 |
+
}
|
19 |
+
}
|
20 |
+
|
21 |
+
/* Custom scrollbar */
|
22 |
+
::-webkit-scrollbar {
|
23 |
+
width: 8px;
|
24 |
+
height: 8px;
|
25 |
+
}
|
26 |
+
|
27 |
+
::-webkit-scrollbar-track {
|
28 |
+
background: #f1f1f1;
|
29 |
+
}
|
30 |
+
|
31 |
+
::-webkit-scrollbar-thumb {
|
32 |
+
background: #888;
|
33 |
+
border-radius: 4px;
|
34 |
+
}
|
35 |
+
|
36 |
+
::-webkit-scrollbar-thumb:hover {
|
37 |
+
background: #555;
|
38 |
+
}
|
39 |
+
|
40 |
+
/* Dark mode scrollbar */
|
41 |
+
@media (prefers-color-scheme: dark) {
|
42 |
+
::-webkit-scrollbar-track {
|
43 |
+
background: #2d2d2d;
|
44 |
+
}
|
45 |
+
|
46 |
+
::-webkit-scrollbar-thumb {
|
47 |
+
background: #666;
|
48 |
+
}
|
49 |
+
|
50 |
+
::-webkit-scrollbar-thumb:hover {
|
51 |
+
background: #888;
|
52 |
+
}
|
53 |
+
}
|
54 |
+
|
55 |
+
/* Toast notifications */
|
56 |
+
.toast {
|
57 |
+
position: fixed;
|
58 |
+
bottom: 20px;
|
59 |
+
right: 20px;
|
60 |
+
padding: 12px 24px;
|
61 |
+
border-radius: 4px;
|
62 |
+
color: white;
|
63 |
+
font-weight: 500;
|
64 |
+
z-index: 1000;
|
65 |
+
animation: slideIn 0.3s ease-out;
|
66 |
+
}
|
67 |
+
|
68 |
+
.toast.success {
|
69 |
+
background-color: #10B981;
|
70 |
+
}
|
71 |
+
|
72 |
+
.toast.error {
|
73 |
+
background-color: #EF4444;
|
74 |
+
}
|
75 |
+
|
76 |
+
@keyframes slideIn {
|
77 |
+
from {
|
78 |
+
transform: translateX(100%);
|
79 |
+
opacity: 0;
|
80 |
+
}
|
81 |
+
to {
|
82 |
+
transform: translateX(0);
|
83 |
+
opacity: 1;
|
84 |
+
}
|
85 |
+
}
|
86 |
+
</style>
|
87 |
+
</head>
|
88 |
+
<body class="bg-gray-50 dark:bg-gray-900 min-h-screen">
|
89 |
+
<!-- Navigation -->
|
90 |
+
<nav class="bg-white dark:bg-gray-800 shadow-sm">
|
91 |
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
92 |
+
<div class="flex justify-between h-16">
|
93 |
+
<div class="flex">
|
94 |
+
<!-- Logo -->
|
95 |
+
<div class="flex-shrink-0 flex items-center">
|
96 |
+
<a href="/" class="text-xl font-bold text-blue-600 dark:text-blue-400">
|
97 |
+
{{ app_name }}
|
98 |
+
</a>
|
99 |
+
</div>
|
100 |
+
|
101 |
+
<!-- Navigation Links -->
|
102 |
+
<div class="hidden sm:ml-6 sm:flex sm:space-x-8">
|
103 |
+
<a href="/" class="inline-flex items-center px-1 pt-1 text-sm font-medium text-gray-900 dark:text-white">
|
104 |
+
Home
|
105 |
+
</a>
|
106 |
+
<a href="/tools" class="inline-flex items-center px-1 pt-1 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">
|
107 |
+
Tools
|
108 |
+
</a>
|
109 |
+
<a href="/credits" class="inline-flex items-center px-1 pt-1 text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">
|
110 |
+
Credits
|
111 |
+
</a>
|
112 |
+
</div>
|
113 |
+
</div>
|
114 |
+
|
115 |
+
<!-- User Menu -->
|
116 |
+
<div class="flex items-center">
|
117 |
+
<div class="flex items-center space-x-4">
|
118 |
+
{% if user %}
|
119 |
+
<span class="text-sm text-gray-700 dark:text-gray-300">
|
120 |
+
Credits: <span id="user-credits">{{ user_credits }}</span>
|
121 |
+
</span>
|
122 |
+
<div class="relative">
|
123 |
+
<button id="user-menu-button" class="flex items-center text-sm text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white">
|
124 |
+
<span class="mr-2">{{ user.username }}</span>
|
125 |
+
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
126 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
127 |
+
</svg>
|
128 |
+
</button>
|
129 |
+
<div id="user-dropdown" class="hidden absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg py-1 z-10">
|
130 |
+
<a href="/profile" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">Profile</a>
|
131 |
+
<a href="/credits" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">Add Credits</a>
|
132 |
+
<a href="/logout" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">Logout</a>
|
133 |
+
</div>
|
134 |
+
</div>
|
135 |
+
{% else %}
|
136 |
+
<a href="/login" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700">
|
137 |
+
Login
|
138 |
+
</a>
|
139 |
+
<a href="/register" class="inline-flex items-center px-4 py-2 text-sm font-medium text-blue-600 dark:text-blue-400 bg-white dark:bg-gray-800 border border-blue-600 dark:border-blue-400 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700">
|
140 |
+
Register
|
141 |
+
</a>
|
142 |
+
{% endif %}
|
143 |
+
</div>
|
144 |
+
</div>
|
145 |
+
</div>
|
146 |
+
</div>
|
147 |
+
</nav>
|
148 |
+
|
149 |
+
<!-- Main Content -->
|
150 |
+
<div class="flex">
|
151 |
+
<!-- Sidebar -->
|
152 |
+
<div class="w-64 bg-white dark:bg-gray-800 shadow-sm h-screen fixed">
|
153 |
+
<div class="p-4">
|
154 |
+
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Tools</h2>
|
155 |
+
<nav class="space-y-1">
|
156 |
+
{% for tool in tools %}
|
157 |
+
<a href="/tools/{{ tool.id }}" class="flex items-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg">
|
158 |
+
<span class="mr-3">{{ tool.icon }}</span>
|
159 |
+
{{ tool.name }}
|
160 |
+
</a>
|
161 |
+
{% endfor %}
|
162 |
+
</nav>
|
163 |
+
</div>
|
164 |
+
</div>
|
165 |
+
|
166 |
+
<!-- Page Content -->
|
167 |
+
<div class="flex-1 ml-64">
|
168 |
+
{% block content %}{% endblock %}
|
169 |
+
</div>
|
170 |
+
</div>
|
171 |
+
|
172 |
+
<!-- Toast Container -->
|
173 |
+
<div id="toast-container"></div>
|
174 |
+
|
175 |
+
<!-- Common JavaScript -->
|
176 |
+
<script>
|
177 |
+
// Show toast notification
|
178 |
+
function showToast(message, type = 'success') {
|
179 |
+
const container = document.getElementById('toast-container');
|
180 |
+
const toast = document.createElement('div');
|
181 |
+
toast.className = `toast ${type}`;
|
182 |
+
toast.textContent = message;
|
183 |
+
container.appendChild(toast);
|
184 |
+
|
185 |
+
setTimeout(() => {
|
186 |
+
toast.remove();
|
187 |
+
}, 3000);
|
188 |
+
}
|
189 |
+
|
190 |
+
// User menu toggle
|
191 |
+
const userMenuButton = document.getElementById('user-menu-button');
|
192 |
+
const userDropdown = document.getElementById('user-dropdown');
|
193 |
+
|
194 |
+
if (userMenuButton && userDropdown) {
|
195 |
+
userMenuButton.addEventListener('click', function() {
|
196 |
+
userDropdown.classList.toggle('hidden');
|
197 |
+
});
|
198 |
+
|
199 |
+
// Close the dropdown when clicking outside
|
200 |
+
document.addEventListener('click', function(event) {
|
201 |
+
if (!userMenuButton.contains(event.target) && !userDropdown.contains(event.target)) {
|
202 |
+
userDropdown.classList.add('hidden');
|
203 |
+
}
|
204 |
+
});
|
205 |
+
}
|
206 |
+
|
207 |
+
// Dark mode toggle
|
208 |
+
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
209 |
+
document.documentElement.classList.add('dark');
|
210 |
+
} else {
|
211 |
+
document.documentElement.classList.remove('dark');
|
212 |
+
}
|
213 |
+
</script>
|
214 |
+
|
215 |
+
{% block scripts %}{% endblock %}
|
216 |
+
</body>
|
217 |
+
</html>
|
templates/credits.html
ADDED
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends "base.html" %}
|
2 |
+
|
3 |
+
{% block title %}Credits{% endblock %}
|
4 |
+
|
5 |
+
{% block content %}
|
6 |
+
<div class="container mx-auto px-4 py-8">
|
7 |
+
<!-- Header -->
|
8 |
+
<div class="flex items-center justify-between mb-8">
|
9 |
+
<div>
|
10 |
+
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Credits</h1>
|
11 |
+
<p class="mt-2 text-gray-600 dark:text-gray-300">Manage your credits for using AI tools</p>
|
12 |
+
</div>
|
13 |
+
<div class="text-right">
|
14 |
+
<div class="text-2xl font-bold text-blue-600">{{ user_credits }}</div>
|
15 |
+
<div class="text-sm text-gray-500">credits available</div>
|
16 |
+
</div>
|
17 |
+
</div>
|
18 |
+
|
19 |
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
20 |
+
<!-- Earn Credits -->
|
21 |
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
|
22 |
+
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Earn Credits</h2>
|
23 |
+
|
24 |
+
<!-- Daily Rewards -->
|
25 |
+
<div class="mb-6">
|
26 |
+
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Daily Rewards</h3>
|
27 |
+
<p class="text-gray-600 dark:text-gray-300 mb-4">
|
28 |
+
Watch ads to earn up to 30 credits per day (3 rewards of 10 credits each)
|
29 |
+
</p>
|
30 |
+
<div class="flex items-center justify-between mb-4">
|
31 |
+
<div class="text-sm text-gray-500 dark:text-gray-400">
|
32 |
+
<span id="daily-count">0</span>/3 rewards claimed today
|
33 |
+
</div>
|
34 |
+
<div id="daily-cooldown" class="text-sm text-gray-500 dark:text-gray-400 hidden">
|
35 |
+
Next reward in: <span id="cooldown-timer">5:00</span>
|
36 |
+
</div>
|
37 |
+
</div>
|
38 |
+
<button id="daily-reward-btn"
|
39 |
+
class="w-full py-3 px-4 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed">
|
40 |
+
Watch Ad for 10 Credits
|
41 |
+
</button>
|
42 |
+
</div>
|
43 |
+
|
44 |
+
<!-- Special Reward -->
|
45 |
+
<div class="mb-6">
|
46 |
+
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Special Reward</h3>
|
47 |
+
<p class="text-gray-600 dark:text-gray-300 mb-4">
|
48 |
+
Watch a longer ad to earn 25 bonus credits (available once per day)
|
49 |
+
</p>
|
50 |
+
<button id="special-reward-btn"
|
51 |
+
class="w-full py-3 px-4 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50 disabled:cursor-not-allowed">
|
52 |
+
Watch Special Ad for 25 Credits
|
53 |
+
</button>
|
54 |
+
</div>
|
55 |
+
|
56 |
+
<!-- Referral Program -->
|
57 |
+
<div>
|
58 |
+
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-2">Referral Program</h3>
|
59 |
+
<p class="text-gray-600 dark:text-gray-300 mb-4">
|
60 |
+
Invite friends to earn 50 credits for each referral
|
61 |
+
</p>
|
62 |
+
<div class="flex items-center space-x-4">
|
63 |
+
<input type="text" id="referral-link"
|
64 |
+
value="{{ referral_link }}"
|
65 |
+
class="flex-1 px-4 py-2 text-gray-900 dark:text-white bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
66 |
+
readonly>
|
67 |
+
<button id="copy-referral"
|
68 |
+
class="px-4 py-2 text-sm font-medium text-white bg-green-600 rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
|
69 |
+
Copy
|
70 |
+
</button>
|
71 |
+
</div>
|
72 |
+
</div>
|
73 |
+
</div>
|
74 |
+
|
75 |
+
<!-- Purchase Credits -->
|
76 |
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
|
77 |
+
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Purchase Credits</h2>
|
78 |
+
|
79 |
+
<!-- Credit Packages -->
|
80 |
+
<div class="space-y-4">
|
81 |
+
{% for package in credit_packages %}
|
82 |
+
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
83 |
+
<div class="flex items-center justify-between mb-2">
|
84 |
+
<div>
|
85 |
+
<h3 class="text-lg font-medium text-gray-900 dark:text-white">{{ package.name }}</h3>
|
86 |
+
<p class="text-sm text-gray-500 dark:text-gray-400">{{ package.description }}</p>
|
87 |
+
</div>
|
88 |
+
<div class="text-right">
|
89 |
+
<div class="text-xl font-bold text-blue-600">${{ package.price }}</div>
|
90 |
+
<div class="text-sm text-gray-500">{{ package.credits }} credits</div>
|
91 |
+
</div>
|
92 |
+
</div>
|
93 |
+
<button onclick="purchaseCredits('{{ package.id }}')"
|
94 |
+
class="w-full py-2 px-4 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
95 |
+
Purchase
|
96 |
+
</button>
|
97 |
+
</div>
|
98 |
+
{% endfor %}
|
99 |
+
</div>
|
100 |
+
|
101 |
+
<!-- Payment Methods -->
|
102 |
+
<div class="mt-6">
|
103 |
+
<h3 class="text-sm font-medium text-gray-900 dark:text-white mb-2">Accepted Payment Methods</h3>
|
104 |
+
<div class="flex items-center space-x-4">
|
105 |
+
<svg class="h-8 w-8 text-gray-400" fill="currentColor" viewBox="0 0 24 24">
|
106 |
+
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
|
107 |
+
</svg>
|
108 |
+
<svg class="h-8 w-8 text-gray-400" fill="currentColor" viewBox="0 0 24 24">
|
109 |
+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>
|
110 |
+
</svg>
|
111 |
+
<svg class="h-8 w-8 text-gray-400" fill="currentColor" viewBox="0 0 24 24">
|
112 |
+
<path d="M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z"/>
|
113 |
+
</svg>
|
114 |
+
</div>
|
115 |
+
</div>
|
116 |
+
</div>
|
117 |
+
</div>
|
118 |
+
|
119 |
+
<!-- Credit History -->
|
120 |
+
<div class="mt-8">
|
121 |
+
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Credit History</h2>
|
122 |
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden">
|
123 |
+
<div class="overflow-x-auto">
|
124 |
+
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
125 |
+
<thead class="bg-gray-50 dark:bg-gray-700">
|
126 |
+
<tr>
|
127 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Date</th>
|
128 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type</th>
|
129 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Amount</th>
|
130 |
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Details</th>
|
131 |
+
</tr>
|
132 |
+
</thead>
|
133 |
+
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
134 |
+
{% for transaction in credit_history %}
|
135 |
+
<tr>
|
136 |
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
137 |
+
{{ transaction.date }}
|
138 |
+
</td>
|
139 |
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
140 |
+
{{ transaction.type }}
|
141 |
+
</td>
|
142 |
+
<td class="px-6 py-4 whitespace-nowrap text-sm {% if transaction.amount > 0 %}text-green-600{% else %}text-red-600{% endif %}">
|
143 |
+
{{ transaction.amount }}
|
144 |
+
</td>
|
145 |
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
146 |
+
{{ transaction.details }}
|
147 |
+
</td>
|
148 |
+
</tr>
|
149 |
+
{% endfor %}
|
150 |
+
</tbody>
|
151 |
+
</table>
|
152 |
+
</div>
|
153 |
+
</div>
|
154 |
+
</div>
|
155 |
+
</div>
|
156 |
+
|
157 |
+
<!-- Ad Modal -->
|
158 |
+
<div id="ad-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center">
|
159 |
+
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-lg w-full mx-4">
|
160 |
+
<div class="flex justify-between items-center mb-4">
|
161 |
+
<h2 class="text-xl font-semibold">Watch Ad</h2>
|
162 |
+
<button id="close-ad-modal" class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
163 |
+
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
164 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
165 |
+
</svg>
|
166 |
+
</button>
|
167 |
+
</div>
|
168 |
+
|
169 |
+
<!-- Ad Container -->
|
170 |
+
<div id="ad-container" class="aspect-video bg-gray-100 dark:bg-gray-700 rounded-lg mb-4 flex items-center justify-center">
|
171 |
+
<div class="text-center">
|
172 |
+
<div class="text-2xl font-bold text-gray-400 mb-2" id="ad-countdown">30</div>
|
173 |
+
<div class="text-sm text-gray-500">seconds remaining</div>
|
174 |
+
</div>
|
175 |
+
</div>
|
176 |
+
|
177 |
+
<!-- Progress Bar -->
|
178 |
+
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5 mb-4">
|
179 |
+
<div id="ad-progress" class="bg-blue-600 h-2.5 rounded-full" style="width: 0%"></div>
|
180 |
+
</div>
|
181 |
+
|
182 |
+
<!-- Status Message -->
|
183 |
+
<div id="ad-status" class="text-center text-sm text-gray-500 dark:text-gray-400">
|
184 |
+
Please watch the ad to earn your reward
|
185 |
+
</div>
|
186 |
+
</div>
|
187 |
+
</div>
|
188 |
+
|
189 |
+
<script>
|
190 |
+
document.addEventListener('DOMContentLoaded', function() {
|
191 |
+
const dailyBtn = document.getElementById('daily-reward-btn');
|
192 |
+
const specialBtn = document.getElementById('special-reward-btn');
|
193 |
+
const adModal = document.getElementById('ad-modal');
|
194 |
+
const closeAdModal = document.getElementById('close-ad-modal');
|
195 |
+
const copyReferralBtn = document.getElementById('copy-referral');
|
196 |
+
const referralLink = document.getElementById('referral-link');
|
197 |
+
|
198 |
+
// Load reward status
|
199 |
+
loadRewardStatus();
|
200 |
+
|
201 |
+
// Set up event listeners
|
202 |
+
dailyBtn.addEventListener('click', () => showAdModal('daily'));
|
203 |
+
specialBtn.addEventListener('click', () => showAdModal('special'));
|
204 |
+
closeAdModal.addEventListener('click', hideAdModal);
|
205 |
+
copyReferralBtn.addEventListener('click', copyReferralLink);
|
206 |
+
|
207 |
+
// Load reward status
|
208 |
+
async function loadRewardStatus() {
|
209 |
+
try {
|
210 |
+
const response = await fetch('/api/ads/status');
|
211 |
+
const data = await response.json();
|
212 |
+
|
213 |
+
if (data.success) {
|
214 |
+
updateDailyRewardStatus(data.daily_rewards);
|
215 |
+
updateSpecialRewardStatus(data.special_reward);
|
216 |
+
}
|
217 |
+
} catch (error) {
|
218 |
+
console.error('Error loading reward status:', error);
|
219 |
+
}
|
220 |
+
}
|
221 |
+
|
222 |
+
// Update daily reward status
|
223 |
+
function updateDailyRewardStatus(status) {
|
224 |
+
const dailyCount = document.getElementById('daily-count');
|
225 |
+
const cooldownDiv = document.getElementById('daily-cooldown');
|
226 |
+
const cooldownTimer = document.getElementById('cooldown-timer');
|
227 |
+
|
228 |
+
dailyCount.textContent = status.count;
|
229 |
+
|
230 |
+
if (status.can_claim) {
|
231 |
+
dailyBtn.disabled = false;
|
232 |
+
cooldownDiv.classList.add('hidden');
|
233 |
+
} else {
|
234 |
+
dailyBtn.disabled = true;
|
235 |
+
cooldownDiv.classList.remove('hidden');
|
236 |
+
updateCooldownTimer(status.cooldown_remaining);
|
237 |
+
}
|
238 |
+
}
|
239 |
+
|
240 |
+
// Update special reward status
|
241 |
+
function updateSpecialRewardStatus(status) {
|
242 |
+
specialBtn.disabled = !status.can_claim;
|
243 |
+
}
|
244 |
+
|
245 |
+
// Show ad modal
|
246 |
+
function showAdModal(type) {
|
247 |
+
currentRewardType = type;
|
248 |
+
adModal.classList.remove('hidden');
|
249 |
+
adModal.classList.add('flex');
|
250 |
+
startAdTimer();
|
251 |
+
}
|
252 |
+
|
253 |
+
// Hide ad modal
|
254 |
+
function hideAdModal() {
|
255 |
+
adModal.classList.add('hidden');
|
256 |
+
adModal.classList.remove('flex');
|
257 |
+
stopAdTimer();
|
258 |
+
resetAdProgress();
|
259 |
+
}
|
260 |
+
|
261 |
+
// Copy referral link
|
262 |
+
function copyReferralLink() {
|
263 |
+
referralLink.select();
|
264 |
+
document.execCommand('copy');
|
265 |
+
showToast('Referral link copied to clipboard!', 'success');
|
266 |
+
}
|
267 |
+
|
268 |
+
// Purchase credits
|
269 |
+
window.purchaseCredits = async function(packageId) {
|
270 |
+
try {
|
271 |
+
const response = await fetch('/api/credits/purchase', {
|
272 |
+
method: 'POST',
|
273 |
+
headers: {
|
274 |
+
'Content-Type': 'application/json'
|
275 |
+
},
|
276 |
+
body: JSON.stringify({
|
277 |
+
package_id: packageId
|
278 |
+
})
|
279 |
+
});
|
280 |
+
|
281 |
+
const data = await response.json();
|
282 |
+
|
283 |
+
if (data.success) {
|
284 |
+
// Redirect to payment page
|
285 |
+
window.location.href = data.payment_url;
|
286 |
+
} else {
|
287 |
+
showToast(data.error || 'Failed to initiate purchase', 'error');
|
288 |
+
}
|
289 |
+
} catch (error) {
|
290 |
+
console.error('Error purchasing credits:', error);
|
291 |
+
showToast('Failed to initiate purchase', 'error');
|
292 |
+
}
|
293 |
+
};
|
294 |
+
});
|
295 |
+
</script>
|
296 |
+
{% endblock %}
|
templates/error.html
ADDED
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>{{ app_name }} - Error</title>
|
7 |
+
<link rel="stylesheet" href="{{ url_for('static', path='/css/styles.css') }}">
|
8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
<div class="app-container">
|
12 |
+
<!-- Sidebar -->
|
13 |
+
<div class="sidebar">
|
14 |
+
<div class="sidebar-header">
|
15 |
+
<h1>{{ app_name }}</h1>
|
16 |
+
<div class="sidebar-subtitle">AI Toolkit</div>
|
17 |
+
</div>
|
18 |
+
|
19 |
+
<!-- Credits Section -->
|
20 |
+
<div class="sidebar-section">
|
21 |
+
<div class="credit-display">
|
22 |
+
<div class="credit-icon">
|
23 |
+
<i class="fas fa-coins"></i>
|
24 |
+
</div>
|
25 |
+
<div>
|
26 |
+
<div class="credit-label">Your Credits</div>
|
27 |
+
<div class="credit-value">{{ user_credits }}</div>
|
28 |
+
</div>
|
29 |
+
</div>
|
30 |
+
</div>
|
31 |
+
|
32 |
+
<!-- Navigation -->
|
33 |
+
<div class="sidebar-section">
|
34 |
+
<div class="sidebar-heading">Navigation</div>
|
35 |
+
<a href="/" class="nav-item">
|
36 |
+
<div class="nav-icon"><i class="fas fa-home"></i></div>
|
37 |
+
<div>Home</div>
|
38 |
+
</a>
|
39 |
+
<a href="#" class="nav-item">
|
40 |
+
<div class="nav-icon"><i class="fas fa-history"></i></div>
|
41 |
+
<div>History</div>
|
42 |
+
</a>
|
43 |
+
<a href="#" class="nav-item">
|
44 |
+
<div class="nav-icon"><i class="fas fa-coins"></i></div>
|
45 |
+
<div>Buy Credits</div>
|
46 |
+
</a>
|
47 |
+
</div>
|
48 |
+
|
49 |
+
<!-- Tools -->
|
50 |
+
<div class="sidebar-section">
|
51 |
+
<div class="sidebar-heading">Tools</div>
|
52 |
+
{% for tool in tools %}
|
53 |
+
<a href="/tool/{{ tool.id }}" class="nav-item">
|
54 |
+
<div class="nav-icon"><i class="{{ tool.icon }}"></i></div>
|
55 |
+
<div>{{ tool.name }}</div>
|
56 |
+
</a>
|
57 |
+
{% endfor %}
|
58 |
+
</div>
|
59 |
+
|
60 |
+
<div class="sidebar-footer">
|
61 |
+
© {{ app_name }} 2023
|
62 |
+
</div>
|
63 |
+
</div>
|
64 |
+
|
65 |
+
<!-- Content -->
|
66 |
+
<div class="content">
|
67 |
+
<div class="content-header">
|
68 |
+
<h1>Error</h1>
|
69 |
+
<p>Something went wrong with your request.</p>
|
70 |
+
</div>
|
71 |
+
|
72 |
+
<div class="error-container">
|
73 |
+
<div class="error-icon">
|
74 |
+
<i class="fas fa-exclamation-triangle"></i>
|
75 |
+
</div>
|
76 |
+
<h2>{{ error_title if error_title is defined else 'Error Occurred' }}</h2>
|
77 |
+
<p>{{ error_description if error_description is defined else 'An unexpected error occurred while processing your request.' }}</p>
|
78 |
+
|
79 |
+
<div class="action-buttons">
|
80 |
+
<a href="/" class="btn btn-primary">Return to Home</a>
|
81 |
+
<button onclick="history.back()" class="btn btn-secondary">Go Back</button>
|
82 |
+
</div>
|
83 |
+
</div>
|
84 |
+
</div>
|
85 |
+
</div>
|
86 |
+
</body>
|
87 |
+
</html>
|
templates/index.html
ADDED
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>{{ app_name }} - AI Tools Platform</title>
|
7 |
+
<link rel="stylesheet" href="{{ url_for('static', path='/css/styles.css') }}">
|
8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
9 |
+
<script src="{{ url_for('static', path='/js/main.js') }}" defer></script>
|
10 |
+
</head>
|
11 |
+
<body>
|
12 |
+
<div class="app-container">
|
13 |
+
<!-- Sidebar -->
|
14 |
+
<div class="sidebar">
|
15 |
+
<div class="sidebar-header">
|
16 |
+
<h1>{{ app_name }}</h1>
|
17 |
+
<div class="sidebar-subtitle">AI Toolkit</div>
|
18 |
+
</div>
|
19 |
+
|
20 |
+
<!-- User Authentication Section -->
|
21 |
+
<div class="sidebar-section">
|
22 |
+
{% if session_user %}
|
23 |
+
<div class="user-section">
|
24 |
+
<div class="user-info">
|
25 |
+
<div class="user-icon">
|
26 |
+
<i class="fas fa-user-circle"></i>
|
27 |
+
</div>
|
28 |
+
<div>
|
29 |
+
<div class="user-name">{{ session_user.username }}</div>
|
30 |
+
<div class="user-role">{{ session_user.role }}</div>
|
31 |
+
</div>
|
32 |
+
</div>
|
33 |
+
<div class="user-actions">
|
34 |
+
<a href="/logout" class="btn btn-small">Logout</a>
|
35 |
+
{% if session_user.role == "admin" %}
|
36 |
+
<a href="/admin" class="btn btn-small btn-primary">Admin</a>
|
37 |
+
{% endif %}
|
38 |
+
</div>
|
39 |
+
</div>
|
40 |
+
{% else %}
|
41 |
+
<div class="auth-buttons">
|
42 |
+
<a href="/login" class="btn btn-primary">Login</a>
|
43 |
+
<a href="/register" class="btn btn-secondary">Register</a>
|
44 |
+
</div>
|
45 |
+
{% endif %}
|
46 |
+
</div>
|
47 |
+
|
48 |
+
<!-- Credits Section -->
|
49 |
+
<div class="sidebar-section">
|
50 |
+
<div class="credit-display">
|
51 |
+
<div class="credit-icon">
|
52 |
+
<i class="fas fa-coins"></i>
|
53 |
+
</div>
|
54 |
+
<div>
|
55 |
+
<div class="credit-label">Your Credits</div>
|
56 |
+
<div class="credit-value">{{ user_credits }}</div>
|
57 |
+
</div>
|
58 |
+
</div>
|
59 |
+
</div>
|
60 |
+
|
61 |
+
<!-- Navigation -->
|
62 |
+
<div class="sidebar-section">
|
63 |
+
<div class="sidebar-heading">Navigation</div>
|
64 |
+
<a href="/" class="nav-item active">
|
65 |
+
<div class="nav-icon"><i class="fas fa-home"></i></div>
|
66 |
+
<div>Home</div>
|
67 |
+
</a>
|
68 |
+
<a href="#" class="nav-item">
|
69 |
+
<div class="nav-icon"><i class="fas fa-history"></i></div>
|
70 |
+
<div>History</div>
|
71 |
+
</a>
|
72 |
+
<a href="#" class="nav-item">
|
73 |
+
<div class="nav-icon"><i class="fas fa-coins"></i></div>
|
74 |
+
<div>Buy Credits</div>
|
75 |
+
</a>
|
76 |
+
</div>
|
77 |
+
|
78 |
+
<!-- Categories -->
|
79 |
+
<div class="sidebar-section">
|
80 |
+
<div class="sidebar-heading">Categories</div>
|
81 |
+
<a href="#text-tools" class="nav-item" onclick="filterTools('text'); return false;">
|
82 |
+
<div class="nav-icon"><i class="fas fa-font"></i></div>
|
83 |
+
<div>Text Generation</div>
|
84 |
+
</a>
|
85 |
+
<a href="#image-tools" class="nav-item" onclick="filterTools('image'); return false;">
|
86 |
+
<div class="nav-icon"><i class="fas fa-image"></i></div>
|
87 |
+
<div>Image Generation</div>
|
88 |
+
</a>
|
89 |
+
<a href="#audio-tools" class="nav-item" onclick="filterTools('audio'); return false;">
|
90 |
+
<div class="nav-icon"><i class="fas fa-music"></i></div>
|
91 |
+
<div>Audio Tools</div>
|
92 |
+
</a>
|
93 |
+
<a href="#video-tools" class="nav-item" onclick="filterTools('video'); return false;">
|
94 |
+
<div class="nav-icon"><i class="fas fa-video"></i></div>
|
95 |
+
<div>Video Tools</div>
|
96 |
+
</a>
|
97 |
+
<a href="#utility-tools" class="nav-item" onclick="filterTools('utility'); return false;">
|
98 |
+
<div class="nav-icon"><i class="fas fa-tools"></i></div>
|
99 |
+
<div>Utility Tools</div>
|
100 |
+
</a>
|
101 |
+
</div>
|
102 |
+
|
103 |
+
<div class="sidebar-footer">
|
104 |
+
© {{ app_name }} 2023
|
105 |
+
</div>
|
106 |
+
</div>
|
107 |
+
|
108 |
+
<!-- Content -->
|
109 |
+
<div class="content">
|
110 |
+
<div class="content-header">
|
111 |
+
<h1>AI Tools Platform</h1>
|
112 |
+
<p>Select a tool to get started. Each tool requires credits which you can earn by watching ads.</p>
|
113 |
+
|
114 |
+
<div class="search-container">
|
115 |
+
<input type="text" id="tool-search" class="search-input" placeholder="Search for tools..." onkeyup="searchTools()">
|
116 |
+
</div>
|
117 |
+
</div>
|
118 |
+
|
119 |
+
<!-- Tool Grid -->
|
120 |
+
<div class="tools-grid" id="tools-grid">
|
121 |
+
{% for tool in tools %}
|
122 |
+
<a href="/tool/{{ tool.id }}" class="tool-card" data-category="{{ tool.category }}">
|
123 |
+
<div class="tool-icon">
|
124 |
+
<i class="{{ tool.icon }}"></i>
|
125 |
+
</div>
|
126 |
+
<div class="tool-info">
|
127 |
+
<h3 class="tool-name">{{ tool.name }}</h3>
|
128 |
+
<p class="tool-description">{{ tool.description }}</p>
|
129 |
+
<div class="tool-meta">
|
130 |
+
<div class="tool-cost">{{ tool.credits }} credits</div>
|
131 |
+
</div>
|
132 |
+
</div>
|
133 |
+
</a>
|
134 |
+
{% endfor %}
|
135 |
+
</div>
|
136 |
+
|
137 |
+
<!-- No Results Message -->
|
138 |
+
<div id="no-results" style="display: none; text-align: center; margin-top: 40px;">
|
139 |
+
<i class="fas fa-search fa-3x" style="opacity: 0.5; margin-bottom: 15px;"></i>
|
140 |
+
<h3>No tools found</h3>
|
141 |
+
<p>Try different search terms or browse all categories</p>
|
142 |
+
<button class="btn btn-secondary" onclick="resetSearch()">Show All Tools</button>
|
143 |
+
</div>
|
144 |
+
</div>
|
145 |
+
</div>
|
146 |
+
|
147 |
+
<script>
|
148 |
+
// Filter tools by category
|
149 |
+
function filterTools(category) {
|
150 |
+
const toolCards = document.querySelectorAll('.tool-card');
|
151 |
+
const noResults = document.getElementById('no-results');
|
152 |
+
let visibleCount = 0;
|
153 |
+
|
154 |
+
toolCards.forEach(card => {
|
155 |
+
if (category === 'all' || card.dataset.category === category) {
|
156 |
+
card.style.display = 'flex';
|
157 |
+
visibleCount++;
|
158 |
+
} else {
|
159 |
+
card.style.display = 'none';
|
160 |
+
}
|
161 |
+
});
|
162 |
+
|
163 |
+
// Update category nav items
|
164 |
+
document.querySelectorAll('.sidebar .nav-item').forEach(item => {
|
165 |
+
if (item.textContent.trim().toLowerCase().includes(category) ||
|
166 |
+
(category === 'all' && item.textContent.trim() === 'Home')) {
|
167 |
+
item.classList.add('active');
|
168 |
+
} else {
|
169 |
+
item.classList.remove('active');
|
170 |
+
}
|
171 |
+
});
|
172 |
+
|
173 |
+
noResults.style.display = visibleCount === 0 ? 'block' : 'none';
|
174 |
+
}
|
175 |
+
|
176 |
+
// Search tools
|
177 |
+
function searchTools() {
|
178 |
+
const searchTerm = document.getElementById('tool-search').value.toLowerCase();
|
179 |
+
const toolCards = document.querySelectorAll('.tool-card');
|
180 |
+
const noResults = document.getElementById('no-results');
|
181 |
+
let visibleCount = 0;
|
182 |
+
|
183 |
+
toolCards.forEach(card => {
|
184 |
+
const name = card.querySelector('.tool-name').textContent.toLowerCase();
|
185 |
+
const description = card.querySelector('.tool-description').textContent.toLowerCase();
|
186 |
+
|
187 |
+
if (name.includes(searchTerm) || description.includes(searchTerm)) {
|
188 |
+
card.style.display = 'flex';
|
189 |
+
visibleCount++;
|
190 |
+
} else {
|
191 |
+
card.style.display = 'none';
|
192 |
+
}
|
193 |
+
});
|
194 |
+
|
195 |
+
noResults.style.display = visibleCount === 0 ? 'block' : 'none';
|
196 |
+
}
|
197 |
+
|
198 |
+
// Reset search
|
199 |
+
function resetSearch() {
|
200 |
+
document.getElementById('tool-search').value = '';
|
201 |
+
filterTools('all');
|
202 |
+
}
|
203 |
+
|
204 |
+
// Initialize
|
205 |
+
document.addEventListener('DOMContentLoaded', function() {
|
206 |
+
// Set initial active state
|
207 |
+
filterTools('all');
|
208 |
+
});
|
209 |
+
</script>
|
210 |
+
</body>
|
211 |
+
</html>
|
templates/login.html
ADDED
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends "base.html" %}
|
2 |
+
|
3 |
+
{% block content %}
|
4 |
+
<div class="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8">
|
5 |
+
<div class="max-w-md w-full space-y-8">
|
6 |
+
<div>
|
7 |
+
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
|
8 |
+
Sign in to your account
|
9 |
+
</h2>
|
10 |
+
<p class="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
|
11 |
+
Or
|
12 |
+
<a href="#" class="font-medium text-blue-600 hover:text-blue-500">
|
13 |
+
create a new account
|
14 |
+
</a>
|
15 |
+
</p>
|
16 |
+
</div>
|
17 |
+
<form class="mt-8 space-y-6" action="/login" method="POST">
|
18 |
+
<div class="rounded-md shadow-sm -space-y-px">
|
19 |
+
<div>
|
20 |
+
<label for="username" class="sr-only">Username</label>
|
21 |
+
<input id="username" name="username" type="text" required
|
22 |
+
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white rounded-t-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm dark:bg-gray-700"
|
23 |
+
placeholder="Username">
|
24 |
+
</div>
|
25 |
+
<div>
|
26 |
+
<label for="password" class="sr-only">Password</label>
|
27 |
+
<input id="password" name="password" type="password" required
|
28 |
+
class="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white rounded-b-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm dark:bg-gray-700"
|
29 |
+
placeholder="Password">
|
30 |
+
</div>
|
31 |
+
</div>
|
32 |
+
|
33 |
+
<div class="flex items-center justify-between">
|
34 |
+
<div class="flex items-center">
|
35 |
+
<input id="remember-me" name="remember-me" type="checkbox"
|
36 |
+
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 dark:border-gray-600 rounded">
|
37 |
+
<label for="remember-me" class="ml-2 block text-sm text-gray-900 dark:text-gray-300">
|
38 |
+
Remember me
|
39 |
+
</label>
|
40 |
+
</div>
|
41 |
+
|
42 |
+
<div class="text-sm">
|
43 |
+
<a href="#" class="font-medium text-blue-600 hover:text-blue-500">
|
44 |
+
Forgot your password?
|
45 |
+
</a>
|
46 |
+
</div>
|
47 |
+
</div>
|
48 |
+
|
49 |
+
<div>
|
50 |
+
<button type="submit"
|
51 |
+
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
52 |
+
<span class="absolute left-0 inset-y-0 flex items-center pl-3">
|
53 |
+
<svg class="h-5 w-5 text-blue-500 group-hover:text-blue-400" xmlns="http://www.w3.org/2000/svg"
|
54 |
+
viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
55 |
+
<path fill-rule="evenodd"
|
56 |
+
d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
|
57 |
+
clip-rule="evenodd"/>
|
58 |
+
</svg>
|
59 |
+
</span>
|
60 |
+
Sign in
|
61 |
+
</button>
|
62 |
+
</div>
|
63 |
+
</form>
|
64 |
+
|
65 |
+
<div class="mt-6">
|
66 |
+
<div class="relative">
|
67 |
+
<div class="absolute inset-0 flex items-center">
|
68 |
+
<div class="w-full border-t border-gray-300 dark:border-gray-600"></div>
|
69 |
+
</div>
|
70 |
+
<div class="relative flex justify-center text-sm">
|
71 |
+
<span class="px-2 bg-gray-50 dark:bg-gray-900 text-gray-500 dark:text-gray-400">
|
72 |
+
Or continue with
|
73 |
+
</span>
|
74 |
+
</div>
|
75 |
+
</div>
|
76 |
+
|
77 |
+
<div class="mt-6 grid grid-cols-2 gap-3">
|
78 |
+
<div>
|
79 |
+
<a href="#"
|
80 |
+
class="w-full inline-flex justify-center py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600">
|
81 |
+
<span class="sr-only">Sign in with Google</span>
|
82 |
+
<svg class="w-5 h-5" viewBox="0 0 24 24">
|
83 |
+
<path fill="currentColor"
|
84 |
+
d="M12.545,10.239v3.821h5.445c-0.712,2.315-2.647,3.972-5.445,3.972c-3.332,0-6.033-2.701-6.033-6.032s2.701-6.032,6.033-6.032c1.498,0,2.866,0.549,3.921,1.453l2.814-2.814C17.503,2.988,15.139,2,12.545,2C7.021,2,2.543,6.477,2.543,12s4.478,10,10.002,10c8.396,0,10.249-7.85,9.426-11.748L12.545,10.239z"/>
|
85 |
+
</svg>
|
86 |
+
</a>
|
87 |
+
</div>
|
88 |
+
|
89 |
+
<div>
|
90 |
+
<a href="#"
|
91 |
+
class="w-full inline-flex justify-center py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-600">
|
92 |
+
<span class="sr-only">Sign in with GitHub</span>
|
93 |
+
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
94 |
+
<path fill-rule="evenodd"
|
95 |
+
d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z"
|
96 |
+
clip-rule="evenodd"/>
|
97 |
+
</svg>
|
98 |
+
</a>
|
99 |
+
</div>
|
100 |
+
</div>
|
101 |
+
</div>
|
102 |
+
</div>
|
103 |
+
</div>
|
104 |
+
{% endblock %}
|
templates/marketplace.html
ADDED
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends "base.html" %}
|
2 |
+
|
3 |
+
{% block content %}
|
4 |
+
<div class="container mx-auto px-4 py-8">
|
5 |
+
<div class="max-w-7xl mx-auto">
|
6 |
+
<!-- Header -->
|
7 |
+
<div class="flex items-center justify-between mb-8">
|
8 |
+
<div>
|
9 |
+
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Prompt Marketplace</h1>
|
10 |
+
<p class="mt-2 text-gray-600 dark:text-gray-300">Discover and purchase high-quality prompts for various AI tools</p>
|
11 |
+
</div>
|
12 |
+
<div class="text-right">
|
13 |
+
<div class="text-2xl font-bold text-blue-600">{{ user_credits }}</div>
|
14 |
+
<div class="text-sm text-gray-500">credits available</div>
|
15 |
+
</div>
|
16 |
+
</div>
|
17 |
+
|
18 |
+
<!-- Filters and Search -->
|
19 |
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 mb-8">
|
20 |
+
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
21 |
+
<!-- Search -->
|
22 |
+
<div class="flex-1">
|
23 |
+
<div class="relative">
|
24 |
+
<input type="text" id="search-input"
|
25 |
+
class="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 pl-10 pr-4 py-2 focus:ring-blue-500 focus:border-blue-500"
|
26 |
+
placeholder="Search prompts...">
|
27 |
+
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
28 |
+
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
29 |
+
fill="currentColor">
|
30 |
+
<path fill-rule="evenodd"
|
31 |
+
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
32 |
+
clip-rule="evenodd"/>
|
33 |
+
</svg>
|
34 |
+
</div>
|
35 |
+
</div>
|
36 |
+
</div>
|
37 |
+
|
38 |
+
<!-- Category Filter -->
|
39 |
+
<div class="flex-shrink-0">
|
40 |
+
<select id="category-filter"
|
41 |
+
class="rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:ring-blue-500 focus:border-blue-500">
|
42 |
+
<option value="">All Categories</option>
|
43 |
+
<option value="text" {% if category == 'text' %}selected{% endif %}>Text Generation</option>
|
44 |
+
<option value="image" {% if category == 'image' %}selected{% endif %}>Image Generation</option>
|
45 |
+
<option value="code" {% if category == 'code' %}selected{% endif %}>Code Generation</option>
|
46 |
+
</select>
|
47 |
+
</div>
|
48 |
+
|
49 |
+
<!-- Sort By -->
|
50 |
+
<div class="flex-shrink-0">
|
51 |
+
<select id="sort-by"
|
52 |
+
class="rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:ring-blue-500 focus:border-blue-500">
|
53 |
+
<option value="popular" {% if sort_by == 'popular' %}selected{% endif %}>Most Popular</option>
|
54 |
+
<option value="newest" {% if sort_by == 'newest' %}selected{% endif %}>Newest First</option>
|
55 |
+
<option value="rating" {% if sort_by == 'rating' %}selected{% endif %}>Highest Rated</option>
|
56 |
+
</select>
|
57 |
+
</div>
|
58 |
+
</div>
|
59 |
+
</div>
|
60 |
+
|
61 |
+
<!-- Prompts Grid -->
|
62 |
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
63 |
+
{% for prompt in prompts %}
|
64 |
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden prompt-card"
|
65 |
+
data-category="{{ prompt.tool_id }}"
|
66 |
+
data-title="{{ prompt.title|lower }}"
|
67 |
+
data-rating="{{ prompt.avg_rating }}"
|
68 |
+
data-usage="{{ prompt.usage_count }}"
|
69 |
+
data-date="{{ prompt.created_at }}">
|
70 |
+
<!-- Prompt Header -->
|
71 |
+
<div class="p-6">
|
72 |
+
<div class="flex items-start justify-between">
|
73 |
+
<div>
|
74 |
+
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ prompt.title }}</h3>
|
75 |
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">{{ prompt.description }}</p>
|
76 |
+
</div>
|
77 |
+
<div class="flex items-center">
|
78 |
+
<span class="text-yellow-500">★</span>
|
79 |
+
<span class="ml-1 text-sm text-gray-600 dark:text-gray-300">{{ "%.1f"|format(prompt.avg_rating) }}</span>
|
80 |
+
</div>
|
81 |
+
</div>
|
82 |
+
|
83 |
+
<!-- Tags -->
|
84 |
+
<div class="mt-4 flex flex-wrap gap-2">
|
85 |
+
{% for tag in prompt.tags %}
|
86 |
+
<span class="px-2 py-1 text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 rounded-full">
|
87 |
+
{{ tag }}
|
88 |
+
</span>
|
89 |
+
{% endfor %}
|
90 |
+
</div>
|
91 |
+
|
92 |
+
<!-- Usage Stats -->
|
93 |
+
<div class="mt-4 flex items-center text-sm text-gray-500 dark:text-gray-400">
|
94 |
+
<svg class="h-4 w-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
95 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
96 |
+
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
97 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
98 |
+
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
99 |
+
</svg>
|
100 |
+
{{ prompt.usage_count }} uses
|
101 |
+
</div>
|
102 |
+
</div>
|
103 |
+
|
104 |
+
<!-- Preview -->
|
105 |
+
<div class="px-6 py-4 bg-gray-50 dark:bg-gray-700">
|
106 |
+
<div class="text-sm text-gray-600 dark:text-gray-300 line-clamp-3">
|
107 |
+
{{ prompt.content }}
|
108 |
+
</div>
|
109 |
+
</div>
|
110 |
+
|
111 |
+
<!-- Footer -->
|
112 |
+
<div class="px-6 py-4 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700">
|
113 |
+
<div class="flex items-center justify-between">
|
114 |
+
<div class="text-sm text-gray-500 dark:text-gray-400">
|
115 |
+
Created by {{ prompt.creator_id }}
|
116 |
+
</div>
|
117 |
+
<button onclick="purchasePrompt('{{ prompt.id }}')"
|
118 |
+
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
119 |
+
Purchase
|
120 |
+
</button>
|
121 |
+
</div>
|
122 |
+
</div>
|
123 |
+
</div>
|
124 |
+
{% endfor %}
|
125 |
+
</div>
|
126 |
+
|
127 |
+
<!-- Empty State -->
|
128 |
+
<div id="empty-state" class="hidden text-center py-12">
|
129 |
+
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
130 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
131 |
+
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
132 |
+
</svg>
|
133 |
+
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">No prompts found</h3>
|
134 |
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
135 |
+
Try adjusting your search or filter criteria
|
136 |
+
</p>
|
137 |
+
</div>
|
138 |
+
</div>
|
139 |
+
</div>
|
140 |
+
|
141 |
+
<!-- Purchase Confirmation Modal -->
|
142 |
+
<div id="purchase-modal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center">
|
143 |
+
<div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-lg w-full mx-4">
|
144 |
+
<h2 class="text-xl font-semibold mb-4">Confirm Purchase</h2>
|
145 |
+
<p class="text-gray-600 dark:text-gray-300 mb-4">
|
146 |
+
Are you sure you want to purchase this prompt? This will cost 5 credits.
|
147 |
+
</p>
|
148 |
+
<div class="flex justify-end space-x-4">
|
149 |
+
<button id="cancel-purchase"
|
150 |
+
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600">
|
151 |
+
Cancel
|
152 |
+
</button>
|
153 |
+
<button id="confirm-purchase"
|
154 |
+
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700">
|
155 |
+
Confirm Purchase
|
156 |
+
</button>
|
157 |
+
</div>
|
158 |
+
</div>
|
159 |
+
</div>
|
160 |
+
|
161 |
+
<script>
|
162 |
+
let selectedPromptId = null;
|
163 |
+
|
164 |
+
// Initialize the page
|
165 |
+
document.addEventListener('DOMContentLoaded', function() {
|
166 |
+
setupEventListeners();
|
167 |
+
setupFilters();
|
168 |
+
});
|
169 |
+
|
170 |
+
// Set up event listeners
|
171 |
+
function setupEventListeners() {
|
172 |
+
// Purchase modal
|
173 |
+
document.getElementById('cancel-purchase').addEventListener('click', hidePurchaseModal);
|
174 |
+
document.getElementById('confirm-purchase').addEventListener('click', handlePurchase);
|
175 |
+
}
|
176 |
+
|
177 |
+
// Set up filters
|
178 |
+
function setupFilters() {
|
179 |
+
const searchInput = document.getElementById('search-input');
|
180 |
+
const categoryFilter = document.getElementById('category-filter');
|
181 |
+
const sortBy = document.getElementById('sort-by');
|
182 |
+
|
183 |
+
function filterPrompts() {
|
184 |
+
const searchTerm = searchInput.value.toLowerCase();
|
185 |
+
const category = categoryFilter.value;
|
186 |
+
const sortValue = sortBy.value;
|
187 |
+
|
188 |
+
const cards = document.querySelectorAll('.prompt-card');
|
189 |
+
let visibleCount = 0;
|
190 |
+
|
191 |
+
cards.forEach(card => {
|
192 |
+
const title = card.dataset.title;
|
193 |
+
const cardCategory = card.dataset.category;
|
194 |
+
const rating = parseFloat(card.dataset.rating);
|
195 |
+
const usage = parseInt(card.dataset.usage);
|
196 |
+
const date = new Date(card.dataset.date);
|
197 |
+
|
198 |
+
const matchesSearch = title.includes(searchTerm);
|
199 |
+
const matchesCategory = !category || cardCategory === category;
|
200 |
+
|
201 |
+
if (matchesSearch && matchesCategory) {
|
202 |
+
card.style.display = 'block';
|
203 |
+
visibleCount++;
|
204 |
+
} else {
|
205 |
+
card.style.display = 'none';
|
206 |
+
}
|
207 |
+
});
|
208 |
+
|
209 |
+
// Show/hide empty state
|
210 |
+
const emptyState = document.getElementById('empty-state');
|
211 |
+
emptyState.classList.toggle('hidden', visibleCount > 0);
|
212 |
+
|
213 |
+
// Sort visible cards
|
214 |
+
const container = document.querySelector('.grid');
|
215 |
+
const visibleCards = Array.from(cards).filter(card => card.style.display !== 'none');
|
216 |
+
|
217 |
+
visibleCards.sort((a, b) => {
|
218 |
+
switch (sortValue) {
|
219 |
+
case 'rating':
|
220 |
+
return parseFloat(b.dataset.rating) - parseFloat(a.dataset.rating);
|
221 |
+
case 'newest':
|
222 |
+
return new Date(b.dataset.date) - new Date(a.dataset.date);
|
223 |
+
case 'popular':
|
224 |
+
default:
|
225 |
+
return parseInt(b.dataset.usage) - parseInt(a.dataset.usage);
|
226 |
+
}
|
227 |
+
});
|
228 |
+
|
229 |
+
visibleCards.forEach(card => container.appendChild(card));
|
230 |
+
}
|
231 |
+
|
232 |
+
searchInput.addEventListener('input', filterPrompts);
|
233 |
+
categoryFilter.addEventListener('change', filterPrompts);
|
234 |
+
sortBy.addEventListener('change', filterPrompts);
|
235 |
+
}
|
236 |
+
|
237 |
+
// Purchase prompt
|
238 |
+
function purchasePrompt(promptId) {
|
239 |
+
selectedPromptId = promptId;
|
240 |
+
showPurchaseModal();
|
241 |
+
}
|
242 |
+
|
243 |
+
// Show purchase modal
|
244 |
+
function showPurchaseModal() {
|
245 |
+
document.getElementById('purchase-modal').classList.remove('hidden');
|
246 |
+
document.getElementById('purchase-modal').classList.add('flex');
|
247 |
+
}
|
248 |
+
|
249 |
+
// Hide purchase modal
|
250 |
+
function hidePurchaseModal() {
|
251 |
+
document.getElementById('purchase-modal').classList.add('hidden');
|
252 |
+
document.getElementById('purchase-modal').classList.remove('flex');
|
253 |
+
selectedPromptId = null;
|
254 |
+
}
|
255 |
+
|
256 |
+
// Handle purchase
|
257 |
+
async function handlePurchase() {
|
258 |
+
if (!selectedPromptId) return;
|
259 |
+
|
260 |
+
try {
|
261 |
+
const response = await fetch('/api/marketplace/purchase', {
|
262 |
+
method: 'POST',
|
263 |
+
headers: {
|
264 |
+
'Content-Type': 'application/json'
|
265 |
+
},
|
266 |
+
body: JSON.stringify({
|
267 |
+
prompt_id: selectedPromptId
|
268 |
+
})
|
269 |
+
});
|
270 |
+
|
271 |
+
const data = await response.json();
|
272 |
+
|
273 |
+
if (data.success) {
|
274 |
+
showToast('Prompt purchased successfully!', 'success');
|
275 |
+
hidePurchaseModal();
|
276 |
+
// Refresh the page to update credits
|
277 |
+
window.location.reload();
|
278 |
+
} else {
|
279 |
+
showToast(data.error || 'Failed to purchase prompt', 'error');
|
280 |
+
}
|
281 |
+
} catch (error) {
|
282 |
+
console.error('Error purchasing prompt:', error);
|
283 |
+
showToast('Failed to purchase prompt', 'error');
|
284 |
+
}
|
285 |
+
}
|
286 |
+
</script>
|
287 |
+
{% endblock %}
|
templates/register.html
ADDED
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>Register | {{ app_name }}</title>
|
7 |
+
<link rel="stylesheet" href="{{ url_for('static', path='/css/styles.css') }}">
|
8 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
9 |
+
<script src="{{ url_for('static', path='/js/main.js') }}" defer></script>
|
10 |
+
</head>
|
11 |
+
<body>
|
12 |
+
<div class="auth-container">
|
13 |
+
<div class="auth-card">
|
14 |
+
<div class="auth-header">
|
15 |
+
<h1>{{ app_name }}</h1>
|
16 |
+
<div class="auth-subtitle">Create an Account</div>
|
17 |
+
</div>
|
18 |
+
|
19 |
+
{% if error %}
|
20 |
+
<div class="alert alert-error">
|
21 |
+
<i class="fas fa-exclamation-triangle"></i>
|
22 |
+
{{ error }}
|
23 |
+
</div>
|
24 |
+
{% endif %}
|
25 |
+
|
26 |
+
<form class="auth-form" action="/register" method="post">
|
27 |
+
<div class="form-group">
|
28 |
+
<label for="username">Username</label>
|
29 |
+
<div class="input-with-icon">
|
30 |
+
<i class="fas fa-user"></i>
|
31 |
+
<input type="text" id="username" name="username" required value="{{ request.form.username if request.form else '' }}">
|
32 |
+
</div>
|
33 |
+
</div>
|
34 |
+
|
35 |
+
<div class="form-group">
|
36 |
+
<label for="email">Email</label>
|
37 |
+
<div class="input-with-icon">
|
38 |
+
<i class="fas fa-envelope"></i>
|
39 |
+
<input type="email" id="email" name="email" required value="{{ request.form.email if request.form else '' }}">
|
40 |
+
</div>
|
41 |
+
</div>
|
42 |
+
|
43 |
+
<div class="form-group">
|
44 |
+
<label for="password">Password</label>
|
45 |
+
<div class="input-with-icon">
|
46 |
+
<i class="fas fa-lock"></i>
|
47 |
+
<input type="password" id="password" name="password" required>
|
48 |
+
</div>
|
49 |
+
</div>
|
50 |
+
|
51 |
+
<div class="form-group">
|
52 |
+
<label for="confirm_password">Confirm Password</label>
|
53 |
+
<div class="input-with-icon">
|
54 |
+
<i class="fas fa-lock"></i>
|
55 |
+
<input type="password" id="confirm_password" name="confirm_password" required>
|
56 |
+
</div>
|
57 |
+
</div>
|
58 |
+
|
59 |
+
<div class="form-group">
|
60 |
+
<button type="submit" class="btn btn-primary btn-block">Create Account</button>
|
61 |
+
</div>
|
62 |
+
|
63 |
+
<div class="auth-links">
|
64 |
+
<a href="/login">Already have an account? Sign in</a>
|
65 |
+
</div>
|
66 |
+
</form>
|
67 |
+
|
68 |
+
<div class="auth-separator">
|
69 |
+
<div class="line"></div>
|
70 |
+
<div class="text">or</div>
|
71 |
+
<div class="line"></div>
|
72 |
+
</div>
|
73 |
+
|
74 |
+
<div class="social-login">
|
75 |
+
<button class="btn btn-google">
|
76 |
+
<i class="fab fa-google"></i>
|
77 |
+
Sign up with Google
|
78 |
+
</button>
|
79 |
+
<button class="btn btn-facebook">
|
80 |
+
<i class="fab fa-facebook"></i>
|
81 |
+
Sign up with Facebook
|
82 |
+
</button>
|
83 |
+
</div>
|
84 |
+
|
85 |
+
<div class="auth-footer">
|
86 |
+
By creating an account, you agree to the
|
87 |
+
<a href="#">Terms of Service</a> and
|
88 |
+
<a href="#">Privacy Policy</a>
|
89 |
+
</div>
|
90 |
+
</div>
|
91 |
+
</div>
|
92 |
+
</body>
|
93 |
+
</html>
|