NandanData commited on
Commit
2a55e2a
·
verified ·
1 Parent(s): 1c6e4b6

Upload 54 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. app/Readme.md +227 -0
  2. app/__pycache__/app.cpython-310.pyc +0 -0
  3. app/__pycache__/models.cpython-310.pyc +0 -0
  4. app/__pycache__/tools.cpython-310.pyc +0 -0
  5. app/ads/__init__.py +10 -0
  6. app/ads/__pycache__/__init__.cpython-310.pyc +0 -0
  7. app/ads/__pycache__/google_ads.cpython-310.pyc +0 -0
  8. app/ads/ads_config.json +28 -0
  9. app/ads/clicks.json +1 -0
  10. app/ads/google_ads.py +376 -0
  11. app/ads/impressions.json +1 -0
  12. app/app.py +1242 -0
  13. app/magicAi.db +0 -0
  14. app/models.py +21 -0
  15. app/prompts/__init__.py +13 -0
  16. app/prompts/__pycache__/__init__.cpython-310.pyc +0 -0
  17. app/prompts/__pycache__/prompt_marketplace.cpython-310.pyc +0 -0
  18. app/prompts/__pycache__/prompt_templates.cpython-310.pyc +0 -0
  19. app/prompts/marketplace_data/purchases.json +1 -0
  20. app/prompts/marketplace_data/sales.json +1 -0
  21. app/prompts/marketplace_data/stats.json +6 -0
  22. app/prompts/prompt_marketplace.py +728 -0
  23. app/prompts/prompt_templates.py +350 -0
  24. app/prompts/templates/code_assistant.json +37 -0
  25. app/prompts/templates/creative_writing.json +30 -0
  26. app/prompts/templates/general_chat.json +14 -0
  27. app/prompts/templates/image_prompt.json +39 -0
  28. app/providers/__init__.py +41 -0
  29. app/providers/__pycache__/__init__.cpython-310.pyc +0 -0
  30. app/providers/__pycache__/deepseek.cpython-310.pyc +0 -0
  31. app/providers/__pycache__/huggingface.cpython-310.pyc +0 -0
  32. app/providers/__pycache__/openai.cpython-310.pyc +0 -0
  33. app/providers/__pycache__/openrouter.cpython-310.pyc +0 -0
  34. app/providers/deepseek.py +210 -0
  35. app/providers/huggingface.py +193 -0
  36. app/providers/openai.py +178 -0
  37. app/providers/openrouter.py +164 -0
  38. app/requirements.txt +35 -0
  39. app/static/css/styles.css +737 -0
  40. app/static/js/main.js +260 -0
  41. app/templates/ad.html +160 -0
  42. app/templates/ad_reward.html +226 -0
  43. app/templates/admin_dashboard.html +238 -0
  44. app/templates/base.html +217 -0
  45. app/templates/credits.html +296 -0
  46. app/templates/error.html +87 -0
  47. app/templates/index.html +211 -0
  48. app/templates/login.html +104 -0
  49. app/templates/marketplace.html +287 -0
  50. app/templates/register.html +93 -0
app/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
app/__pycache__/app.cpython-310.pyc ADDED
Binary file (26.4 kB). View file
 
app/__pycache__/models.cpython-310.pyc ADDED
Binary file (943 Bytes). View file
 
app/__pycache__/tools.cpython-310.pyc ADDED
Binary file (7.25 kB). View file
 
app/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
+ ]
app/ads/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (270 Bytes). View file
 
app/ads/__pycache__/google_ads.cpython-310.pyc ADDED
Binary file (11.6 kB). View file
 
app/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
+ }
app/ads/clicks.json ADDED
@@ -0,0 +1 @@
 
 
1
+ []
app/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)}")
app/ads/impressions.json ADDED
@@ -0,0 +1 @@
 
 
1
+ []
app/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)
app/magicAi.db ADDED
Binary file (41 kB). View file
 
app/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)
app/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
+ ]
app/prompts/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (407 Bytes). View file
 
app/prompts/__pycache__/prompt_marketplace.cpython-310.pyc ADDED
Binary file (19.7 kB). View file
 
app/prompts/__pycache__/prompt_templates.cpython-310.pyc ADDED
Binary file (10.9 kB). View file
 
app/prompts/marketplace_data/purchases.json ADDED
@@ -0,0 +1 @@
 
 
1
+ []
app/prompts/marketplace_data/sales.json ADDED
@@ -0,0 +1 @@
 
 
1
+ []
app/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
+ }
app/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']}")
app/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
+
app/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
+ }
app/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
+ }
app/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
+ }
app/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
+ }
app/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)
app/providers/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (1.07 kB). View file
 
app/providers/__pycache__/deepseek.cpython-310.pyc ADDED
Binary file (4.39 kB). View file
 
app/providers/__pycache__/huggingface.cpython-310.pyc ADDED
Binary file (4.6 kB). View file
 
app/providers/__pycache__/openai.cpython-310.pyc ADDED
Binary file (4.63 kB). View file
 
app/providers/__pycache__/openrouter.cpython-310.pyc ADDED
Binary file (4.48 kB). View file
 
app/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))
app/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))
app/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))
app/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")
app/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
app/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
+ }
app/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
+ });
app/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
+ &copy; {{ 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>
app/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 %}
app/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 %}
app/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>
app/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 %}
app/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
+ &copy; {{ 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>
app/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
+ &copy; {{ 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>
app/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 %}
app/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 %}
app/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>