Commit
·
b8b7791
1
Parent(s):
9474b94
Rename project from Ad Generator Lite to PsyAdGenesis, updating all relevant references and configurations. Enhance descriptions and metadata to reflect the new branding and focus on ad design. Implement sorting functionality in the gallery for improved ad management.
Browse files- README.md +8 -6
- config.py +2 -2
- frontend/README.md +3 -1
- frontend/app/gallery/[id]/page.tsx +0 -23
- frontend/app/gallery/page.tsx +94 -41
- frontend/app/layout.tsx +2 -2
- frontend/app/page.tsx +6 -4
- frontend/components/generation/AdPreview.tsx +0 -23
- frontend/components/layout/Header.tsx +5 -4
- frontend/store/galleryStore.ts +1 -1
- main.py +8 -8
- services/database.py +1 -1
- services/generator.py +1 -1
- services/image.py +1 -1
- services/r2_storage.py +1 -1
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 🚀
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: purple
|
|
@@ -9,7 +9,9 @@ pinned: false
|
|
| 9 |
---
|
| 10 |
|
| 11 |
|
| 12 |
-
#
|
|
|
|
|
|
|
| 13 |
|
| 14 |
Generate high-converting ad creatives for Home Insurance and GLP-1 niches using psychological triggers and AI-powered image generation.
|
| 15 |
|
|
@@ -91,12 +93,12 @@ This app is configured for deployment on Hugging Face Spaces using Docker.
|
|
| 91 |
- Go to https://huggingface.co/spaces
|
| 92 |
- Click "Create new Space"
|
| 93 |
- Choose "Docker" as the SDK
|
| 94 |
-
- Name your space (e.g., `your-username/
|
| 95 |
|
| 96 |
2. **Push your code** to the Space:
|
| 97 |
```bash
|
| 98 |
-
git clone https://huggingface.co/spaces/your-username/
|
| 99 |
-
cd
|
| 100 |
# Copy your files here
|
| 101 |
git add .
|
| 102 |
git commit -m "Initial commit"
|
|
@@ -140,7 +142,7 @@ LOCAL_IMAGE_RETENTION_HOURS=24 # Images older than this will be auto-deleted
|
|
| 140 |
|
| 141 |
Once deployed, your app will be available at:
|
| 142 |
```
|
| 143 |
-
https://your-username-
|
| 144 |
```
|
| 145 |
|
| 146 |
## API Endpoints
|
|
|
|
| 1 |
---
|
| 2 |
+
title: PsyAdGenesis
|
| 3 |
emoji: 🚀
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: purple
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
|
| 12 |
+
# PsyAdGenesis
|
| 13 |
+
|
| 14 |
+
**Design ads that stop the scroll.**
|
| 15 |
|
| 16 |
Generate high-converting ad creatives for Home Insurance and GLP-1 niches using psychological triggers and AI-powered image generation.
|
| 17 |
|
|
|
|
| 93 |
- Go to https://huggingface.co/spaces
|
| 94 |
- Click "Create new Space"
|
| 95 |
- Choose "Docker" as the SDK
|
| 96 |
+
- Name your space (e.g., `your-username/psyadgenesis`)
|
| 97 |
|
| 98 |
2. **Push your code** to the Space:
|
| 99 |
```bash
|
| 100 |
+
git clone https://huggingface.co/spaces/your-username/psyadgenesis
|
| 101 |
+
cd psyadgenesis
|
| 102 |
# Copy your files here
|
| 103 |
git add .
|
| 104 |
git commit -m "Initial commit"
|
|
|
|
| 142 |
|
| 143 |
Once deployed, your app will be available at:
|
| 144 |
```
|
| 145 |
+
https://your-username-psyadgenesis.hf.space
|
| 146 |
```
|
| 147 |
|
| 148 |
## API Endpoints
|
config.py
CHANGED
|
@@ -22,7 +22,7 @@ class Settings(BaseSettings):
|
|
| 22 |
|
| 23 |
# Database (MongoDB)
|
| 24 |
mongodb_url: Optional[str] = None
|
| 25 |
-
mongodb_db_name: str = "
|
| 26 |
|
| 27 |
# R2 Storage (Cloudflare R2)
|
| 28 |
r2_endpoint: Optional[str] = None
|
|
@@ -42,7 +42,7 @@ class Settings(BaseSettings):
|
|
| 42 |
third_flow_model: str = "gpt-4o" # Model for researcher, creative_director, designer, copywriter
|
| 43 |
# Options: "gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4"
|
| 44 |
|
| 45 |
-
# Image Generation Settings
|
| 46 |
image_model: str = "z-image-turbo" # Z-Image Turbo - fast and high quality
|
| 47 |
# Alternative models: "nano-banana", "nano-banana-pro", "imagen-4-ultra", "recraft-v3", "ideogram-v3", "photon", "seedream-3", "gpt-image-1.5"
|
| 48 |
image_width: int = 1024
|
|
|
|
| 22 |
|
| 23 |
# Database (MongoDB)
|
| 24 |
mongodb_url: Optional[str] = None
|
| 25 |
+
mongodb_db_name: str = "psyadgenesis"
|
| 26 |
|
| 27 |
# R2 Storage (Cloudflare R2)
|
| 28 |
r2_endpoint: Optional[str] = None
|
|
|
|
| 42 |
third_flow_model: str = "gpt-4o" # Model for researcher, creative_director, designer, copywriter
|
| 43 |
# Options: "gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4"
|
| 44 |
|
| 45 |
+
# Image Generation Settings
|
| 46 |
image_model: str = "z-image-turbo" # Z-Image Turbo - fast and high quality
|
| 47 |
# Alternative models: "nano-banana", "nano-banana-pro", "imagen-4-ultra", "recraft-v3", "ideogram-v3", "photon", "seedream-3", "gpt-image-1.5"
|
| 48 |
image_width: int = 1024
|
frontend/README.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
| 1 |
-
#
|
|
|
|
|
|
|
| 2 |
|
| 3 |
Modern Next.js dashboard for generating and managing ad creatives for Home Insurance and GLP-1 niches.
|
| 4 |
|
|
|
|
| 1 |
+
# PsyAdGenesis - Frontend
|
| 2 |
+
|
| 3 |
+
**Design ads that stop the scroll.**
|
| 4 |
|
| 5 |
Modern Next.js dashboard for generating and managing ad creatives for Home Insurance and GLP-1 niches.
|
| 6 |
|
frontend/app/gallery/[id]/page.tsx
CHANGED
|
@@ -365,29 +365,6 @@ export default function AdDetailPage() {
|
|
| 365 |
</div>
|
| 366 |
</div>
|
| 367 |
|
| 368 |
-
{/* Primary Text */}
|
| 369 |
-
{ad.primary_text && (
|
| 370 |
-
<div className="bg-white rounded-2xl shadow-md p-6 border-l-4 border-cyan-500">
|
| 371 |
-
<div className="flex items-start justify-between gap-4 mb-3">
|
| 372 |
-
<h3 className="text-xs font-bold text-cyan-600 uppercase tracking-wider">Primary Text</h3>
|
| 373 |
-
<div className="relative group">
|
| 374 |
-
<Button
|
| 375 |
-
variant="ghost"
|
| 376 |
-
size="sm"
|
| 377 |
-
onClick={() => handleCopyText(ad.primary_text!, "Primary Text")}
|
| 378 |
-
className="text-cyan-500 hover:bg-cyan-50"
|
| 379 |
-
>
|
| 380 |
-
<Copy className="h-4 w-4" />
|
| 381 |
-
</Button>
|
| 382 |
-
<span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-cyan-600 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
|
| 383 |
-
Copy Text
|
| 384 |
-
</span>
|
| 385 |
-
</div>
|
| 386 |
-
</div>
|
| 387 |
-
<p className="text-gray-700 whitespace-pre-line leading-relaxed">{ad.primary_text}</p>
|
| 388 |
-
</div>
|
| 389 |
-
)}
|
| 390 |
-
|
| 391 |
{/* Description */}
|
| 392 |
{ad.description && (
|
| 393 |
<div className="bg-white rounded-2xl shadow-md p-6 border-l-4 border-violet-500">
|
|
|
|
| 365 |
</div>
|
| 366 |
</div>
|
| 367 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
{/* Description */}
|
| 369 |
{ad.description && (
|
| 370 |
<div className="bg-white rounded-2xl shadow-md p-6 border-l-4 border-violet-500">
|
frontend/app/gallery/page.tsx
CHANGED
|
@@ -8,7 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
|
|
| 8 |
import { listAds, deleteAd } from "@/lib/api/endpoints";
|
| 9 |
import { useGalleryStore } from "@/store/galleryStore";
|
| 10 |
import { toast } from "react-hot-toast";
|
| 11 |
-
import { Download, Trash2, CheckSquare, Square } from "lucide-react";
|
| 12 |
import type { AdFilters } from "@/types";
|
| 13 |
|
| 14 |
export default function GalleryPage() {
|
|
@@ -18,10 +18,12 @@ export default function GalleryPage() {
|
|
| 18 |
limit,
|
| 19 |
offset,
|
| 20 |
filters,
|
|
|
|
| 21 |
selectedAds,
|
| 22 |
isLoading,
|
| 23 |
setAds,
|
| 24 |
setFilters,
|
|
|
|
| 25 |
setOffset,
|
| 26 |
toggleAdSelection,
|
| 27 |
clearSelection,
|
|
@@ -63,6 +65,34 @@ export default function GalleryPage() {
|
|
| 63 |
);
|
| 64 |
}
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
// Use the total from backend (it's already filtered by niche and generation_method)
|
| 67 |
// For search, we show the filtered count but this is only for current page
|
| 68 |
// In a production app, you'd want server-side search for accurate totals
|
|
@@ -74,7 +104,7 @@ export default function GalleryPage() {
|
|
| 74 |
} finally {
|
| 75 |
setIsLoading(false);
|
| 76 |
}
|
| 77 |
-
}, [filters, limit, offset, setAds, setIsLoading]);
|
| 78 |
|
| 79 |
useEffect(() => {
|
| 80 |
loadAds();
|
|
@@ -108,61 +138,84 @@ export default function GalleryPage() {
|
|
| 108 |
|
| 109 |
return (
|
| 110 |
<div className="min-h-screen pb-12">
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
<div className="
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
<h1 className="text-4xl md:text-5xl font-extrabold mb-4">
|
| 117 |
<span className="gradient-text">Gallery</span>
|
| 118 |
</h1>
|
| 119 |
-
<p className="text-
|
| 120 |
{total} {total === 1 ? "ad" : "ads"} total
|
| 121 |
</p>
|
| 122 |
</div>
|
| 123 |
</div>
|
| 124 |
-
</div>
|
| 125 |
|
| 126 |
-
|
| 127 |
-
<div className="mb-
|
| 128 |
-
<div className="flex items-center justify-between">
|
| 129 |
-
<div className="flex items-center
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
</Button>
|
| 147 |
)}
|
| 148 |
</div>
|
| 149 |
</div>
|
| 150 |
-
</div>
|
| 151 |
|
| 152 |
<FilterBar filters={filters} onFiltersChange={setFilters} />
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
|
| 163 |
{/* Pagination */}
|
| 164 |
{totalPages > 1 && (
|
| 165 |
-
<div className="mt-8 flex items-center justify-center
|
| 166 |
<Button
|
| 167 |
variant="outline"
|
| 168 |
size="sm"
|
|
@@ -171,7 +224,7 @@ export default function GalleryPage() {
|
|
| 171 |
>
|
| 172 |
Previous
|
| 173 |
</Button>
|
| 174 |
-
<span className="text-sm text-gray-600">
|
| 175 |
Page {currentPage} of {totalPages}
|
| 176 |
</span>
|
| 177 |
<Button
|
|
|
|
| 8 |
import { listAds, deleteAd } from "@/lib/api/endpoints";
|
| 9 |
import { useGalleryStore } from "@/store/galleryStore";
|
| 10 |
import { toast } from "react-hot-toast";
|
| 11 |
+
import { Download, Trash2, CheckSquare, Square, ArrowUpDown } from "lucide-react";
|
| 12 |
import type { AdFilters } from "@/types";
|
| 13 |
|
| 14 |
export default function GalleryPage() {
|
|
|
|
| 18 |
limit,
|
| 19 |
offset,
|
| 20 |
filters,
|
| 21 |
+
sortOptions,
|
| 22 |
selectedAds,
|
| 23 |
isLoading,
|
| 24 |
setAds,
|
| 25 |
setFilters,
|
| 26 |
+
setSortOptions,
|
| 27 |
setOffset,
|
| 28 |
toggleAdSelection,
|
| 29 |
clearSelection,
|
|
|
|
| 65 |
);
|
| 66 |
}
|
| 67 |
|
| 68 |
+
// Sort ads by selected sort options
|
| 69 |
+
filteredAds.sort((a, b) => {
|
| 70 |
+
const { field, direction } = sortOptions;
|
| 71 |
+
let aValue: any = a[field as keyof typeof a];
|
| 72 |
+
let bValue: any = b[field as keyof typeof b];
|
| 73 |
+
|
| 74 |
+
// Handle date sorting
|
| 75 |
+
if (field === "created_at") {
|
| 76 |
+
aValue = aValue ? new Date(aValue).getTime() : 0;
|
| 77 |
+
bValue = bValue ? new Date(bValue).getTime() : 0;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
// Handle string sorting (case-insensitive)
|
| 81 |
+
if (typeof aValue === "string" && typeof bValue === "string") {
|
| 82 |
+
aValue = aValue.toLowerCase();
|
| 83 |
+
bValue = bValue.toLowerCase();
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// Handle null/undefined values
|
| 87 |
+
if (aValue == null) aValue = "";
|
| 88 |
+
if (bValue == null) bValue = "";
|
| 89 |
+
|
| 90 |
+
// Compare values
|
| 91 |
+
if (aValue < bValue) return direction === "asc" ? -1 : 1;
|
| 92 |
+
if (aValue > bValue) return direction === "asc" ? 1 : -1;
|
| 93 |
+
return 0;
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
// Use the total from backend (it's already filtered by niche and generation_method)
|
| 97 |
// For search, we show the filtered count but this is only for current page
|
| 98 |
// In a production app, you'd want server-side search for accurate totals
|
|
|
|
| 104 |
} finally {
|
| 105 |
setIsLoading(false);
|
| 106 |
}
|
| 107 |
+
}, [filters, sortOptions, limit, offset, setAds, setIsLoading]);
|
| 108 |
|
| 109 |
useEffect(() => {
|
| 110 |
loadAds();
|
|
|
|
| 138 |
|
| 139 |
return (
|
| 140 |
<div className="min-h-screen pb-12">
|
| 141 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-8">
|
| 142 |
+
{/* Header with title and total */}
|
| 143 |
+
<div className="flex items-center justify-between mb-6">
|
| 144 |
+
<div>
|
| 145 |
+
<h1 className="text-3xl md:text-4xl font-extrabold mb-2">
|
|
|
|
| 146 |
<span className="gradient-text">Gallery</span>
|
| 147 |
</h1>
|
| 148 |
+
<p className="text-sm text-gray-500">
|
| 149 |
{total} {total === 1 ? "ad" : "ads"} total
|
| 150 |
</p>
|
| 151 |
</div>
|
| 152 |
</div>
|
|
|
|
| 153 |
|
| 154 |
+
{/* Actions Bar */}
|
| 155 |
+
<div className="mb-6">
|
| 156 |
+
<div className="flex items-center justify-between flex-wrap gap-4">
|
| 157 |
+
<div className="flex items-center gap-3">
|
| 158 |
+
{selectedAds.length > 0 ? (
|
| 159 |
+
<>
|
| 160 |
+
<Button variant="outline" size="sm" onClick={clearSelection}>
|
| 161 |
+
<div className="flex items-center gap-2">
|
| 162 |
+
<Square className="h-4 w-4" />
|
| 163 |
+
<span>Deselect ({selectedAds.length})</span>
|
| 164 |
+
</div>
|
| 165 |
+
</Button>
|
| 166 |
+
<Button variant="danger" size="sm" onClick={handleBulkDelete}>
|
| 167 |
+
<div className="flex items-center gap-2">
|
| 168 |
+
<Trash2 className="h-4 w-4" />
|
| 169 |
+
<span>Delete Selected</span>
|
| 170 |
+
</div>
|
| 171 |
+
</Button>
|
| 172 |
+
</>
|
| 173 |
+
) : (
|
| 174 |
+
ads.length > 0 && (
|
| 175 |
+
<Button variant="outline" size="sm" onClick={selectAll}>
|
| 176 |
+
<div className="flex items-center gap-2">
|
| 177 |
+
<CheckSquare className="h-4 w-4" />
|
| 178 |
+
<span>Select All</span>
|
| 179 |
+
</div>
|
| 180 |
+
</Button>
|
| 181 |
+
)
|
| 182 |
+
)}
|
| 183 |
+
</div>
|
| 184 |
+
|
| 185 |
+
{/* Sort Controls */}
|
| 186 |
+
{sortOptions && (
|
| 187 |
+
<Button
|
| 188 |
+
variant="outline"
|
| 189 |
+
size="sm"
|
| 190 |
+
onClick={() => setSortOptions({
|
| 191 |
+
field: "created_at",
|
| 192 |
+
direction: sortOptions.direction === "desc" ? "asc" : "desc"
|
| 193 |
+
})}
|
| 194 |
+
className="flex items-center gap-2"
|
| 195 |
+
>
|
| 196 |
+
<ArrowUpDown className="h-4 w-4" />
|
| 197 |
+
<span className="text-sm">
|
| 198 |
+
{sortOptions.direction === "desc" ? "Newest First" : "Oldest First"}
|
| 199 |
+
</span>
|
| 200 |
</Button>
|
| 201 |
)}
|
| 202 |
</div>
|
| 203 |
</div>
|
|
|
|
| 204 |
|
| 205 |
<FilterBar filters={filters} onFiltersChange={setFilters} />
|
| 206 |
|
| 207 |
+
<div className="mt-6">
|
| 208 |
+
<GalleryGrid
|
| 209 |
+
ads={ads}
|
| 210 |
+
selectedAds={selectedAds}
|
| 211 |
+
onAdSelect={toggleAdSelection}
|
| 212 |
+
isLoading={isLoading}
|
| 213 |
+
/>
|
| 214 |
+
</div>
|
| 215 |
|
| 216 |
{/* Pagination */}
|
| 217 |
{totalPages > 1 && (
|
| 218 |
+
<div className="mt-8 flex items-center justify-center gap-4">
|
| 219 |
<Button
|
| 220 |
variant="outline"
|
| 221 |
size="sm"
|
|
|
|
| 224 |
>
|
| 225 |
Previous
|
| 226 |
</Button>
|
| 227 |
+
<span className="text-sm text-gray-600 font-medium">
|
| 228 |
Page {currentPage} of {totalPages}
|
| 229 |
</span>
|
| 230 |
<Button
|
frontend/app/layout.tsx
CHANGED
|
@@ -10,8 +10,8 @@ const inter = Inter({
|
|
| 10 |
});
|
| 11 |
|
| 12 |
export const metadata: Metadata = {
|
| 13 |
-
title: "
|
| 14 |
-
description: "Generate high-converting ad creatives for Home Insurance and GLP-1 niches",
|
| 15 |
};
|
| 16 |
|
| 17 |
export default function RootLayout({
|
|
|
|
| 10 |
});
|
| 11 |
|
| 12 |
export const metadata: Metadata = {
|
| 13 |
+
title: "PsyAdGenesis",
|
| 14 |
+
description: "Design ads that stop the scroll. Generate high-converting ad creatives for Home Insurance and GLP-1 niches using psychological triggers and AI-powered image generation.",
|
| 15 |
};
|
| 16 |
|
| 17 |
export default function RootLayout({
|
frontend/app/page.tsx
CHANGED
|
@@ -155,11 +155,13 @@ export default function Dashboard() {
|
|
| 155 |
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
| 156 |
<div className="text-center animate-fade-in">
|
| 157 |
<h1 className="text-5xl md:text-6xl font-extrabold mb-4">
|
| 158 |
-
<span className="gradient-text">
|
| 159 |
-
<span className="text-gray-900"> Breakthrough</span>
|
| 160 |
</h1>
|
| 161 |
-
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
| 163 |
</p>
|
| 164 |
</div>
|
| 165 |
</div>
|
|
|
|
| 155 |
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
| 156 |
<div className="text-center animate-fade-in">
|
| 157 |
<h1 className="text-5xl md:text-6xl font-extrabold mb-4">
|
| 158 |
+
<span className="gradient-text">PsyAdGenesis</span>
|
|
|
|
| 159 |
</h1>
|
| 160 |
+
<p className="text-xl text-gray-600 max-w-2xl mx-auto font-medium">
|
| 161 |
+
Design ads that stop the scroll.
|
| 162 |
+
</p>
|
| 163 |
+
<p className="text-lg text-gray-500 max-w-2xl mx-auto mt-2">
|
| 164 |
+
Generate high-converting ad creatives for Home Insurance and GLP-1 niches with AI-powered generation
|
| 165 |
</p>
|
| 166 |
</div>
|
| 167 |
</div>
|
frontend/components/generation/AdPreview.tsx
CHANGED
|
@@ -261,29 +261,6 @@ export const AdPreview: React.FC<AdPreviewProps> = ({ ad }) => {
|
|
| 261 |
</h1>
|
| 262 |
</div>
|
| 263 |
|
| 264 |
-
{/* Primary Text */}
|
| 265 |
-
{ad.primary_text && (
|
| 266 |
-
<div className="bg-white rounded-2xl shadow-md p-6 border-l-4 border-cyan-500">
|
| 267 |
-
<div className="flex items-start justify-between gap-4 mb-3">
|
| 268 |
-
<h3 className="text-xs font-bold text-cyan-600 uppercase tracking-wider">Primary Text</h3>
|
| 269 |
-
<div className="relative group">
|
| 270 |
-
<Button
|
| 271 |
-
variant="ghost"
|
| 272 |
-
size="sm"
|
| 273 |
-
onClick={() => handleCopyText(ad.primary_text!, "Primary Text")}
|
| 274 |
-
className="text-cyan-500 hover:bg-cyan-50"
|
| 275 |
-
>
|
| 276 |
-
<Copy className="h-4 w-4" />
|
| 277 |
-
</Button>
|
| 278 |
-
<span className="absolute -bottom-8 left-1/2 -translate-x-1/2 px-2 py-1 bg-cyan-600 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-10">
|
| 279 |
-
Copy Text
|
| 280 |
-
</span>
|
| 281 |
-
</div>
|
| 282 |
-
</div>
|
| 283 |
-
<p className="text-gray-700 whitespace-pre-line leading-relaxed">{ad.primary_text}</p>
|
| 284 |
-
</div>
|
| 285 |
-
)}
|
| 286 |
-
|
| 287 |
{/* Description */}
|
| 288 |
{ad.description && (
|
| 289 |
<div className="bg-white rounded-2xl shadow-md p-6 border-l-4 border-violet-500">
|
|
|
|
| 261 |
</h1>
|
| 262 |
</div>
|
| 263 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
{/* Description */}
|
| 265 |
{ad.description && (
|
| 266 |
<div className="bg-white rounded-2xl shadow-md p-6 border-l-4 border-violet-500">
|
frontend/components/layout/Header.tsx
CHANGED
|
@@ -35,7 +35,7 @@ export const Header: React.FC = () => {
|
|
| 35 |
<div className="absolute inset-0 bg-blue-500/20 rounded-full blur-xl group-hover:bg-cyan-500/20 transition-colors duration-300"></div>
|
| 36 |
</div>
|
| 37 |
<span className="text-2xl font-bold gradient-text group-hover:scale-105 transition-transform duration-300">
|
| 38 |
-
|
| 39 |
</span>
|
| 40 |
</Link>
|
| 41 |
</div>
|
|
@@ -77,10 +77,11 @@ export const Header: React.FC = () => {
|
|
| 77 |
variant="outline"
|
| 78 |
size="sm"
|
| 79 |
onClick={handleLogout}
|
| 80 |
-
className="flex items-center space-x-2"
|
| 81 |
>
|
| 82 |
-
<
|
| 83 |
-
|
|
|
|
|
|
|
| 84 |
</Button>
|
| 85 |
</div>
|
| 86 |
) : (
|
|
|
|
| 35 |
<div className="absolute inset-0 bg-blue-500/20 rounded-full blur-xl group-hover:bg-cyan-500/20 transition-colors duration-300"></div>
|
| 36 |
</div>
|
| 37 |
<span className="text-2xl font-bold gradient-text group-hover:scale-105 transition-transform duration-300">
|
| 38 |
+
PsyAdGenesis
|
| 39 |
</span>
|
| 40 |
</Link>
|
| 41 |
</div>
|
|
|
|
| 77 |
variant="outline"
|
| 78 |
size="sm"
|
| 79 |
onClick={handleLogout}
|
|
|
|
| 80 |
>
|
| 81 |
+
<div className="flex items-center space-x-2">
|
| 82 |
+
<LogOut className="h-4 w-4" />
|
| 83 |
+
<span className="hidden sm:inline">Logout</span>
|
| 84 |
+
</div>
|
| 85 |
</Button>
|
| 86 |
</div>
|
| 87 |
) : (
|
frontend/store/galleryStore.ts
CHANGED
|
@@ -50,7 +50,7 @@ export const useGalleryStore = create<GalleryState>((set, get) => ({
|
|
| 50 |
offset: 0, // Reset to first page when filters change
|
| 51 |
})),
|
| 52 |
|
| 53 |
-
setSortOptions: (sort) => set({ sortOptions: sort }),
|
| 54 |
|
| 55 |
toggleAdSelection: (adId) => set((state) => ({
|
| 56 |
selectedAds: state.selectedAds.includes(adId)
|
|
|
|
| 50 |
offset: 0, // Reset to first page when filters change
|
| 51 |
})),
|
| 52 |
|
| 53 |
+
setSortOptions: (sort) => set({ sortOptions: sort, offset: 0 }), // Reset to first page when sort changes
|
| 54 |
|
| 55 |
toggleAdSelection: (adId) => set((state) => ({
|
| 56 |
selectedAds: state.selectedAds.includes(adId)
|
main.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
-
|
| 4 |
Saves all ads to Neon PostgreSQL database with image URLs
|
| 5 |
"""
|
| 6 |
|
|
@@ -40,7 +40,7 @@ api_logger = logging.getLogger("api")
|
|
| 40 |
async def lifespan(app: FastAPI):
|
| 41 |
"""Startup and shutdown events."""
|
| 42 |
# Startup: Connect to database
|
| 43 |
-
print("Starting
|
| 44 |
await db_service.connect()
|
| 45 |
yield
|
| 46 |
# Shutdown: Disconnect from database
|
|
@@ -50,8 +50,8 @@ async def lifespan(app: FastAPI):
|
|
| 50 |
|
| 51 |
# Create FastAPI app with lifespan for database connection
|
| 52 |
app = FastAPI(
|
| 53 |
-
title="
|
| 54 |
-
description="Generate high-converting ad creatives for Home Insurance and GLP-1 niches using psychological triggers
|
| 55 |
version="2.0.0",
|
| 56 |
lifespan=lifespan,
|
| 57 |
)
|
|
@@ -72,7 +72,7 @@ if os.getenv("CORS_ORIGINS"):
|
|
| 72 |
|
| 73 |
# For Hugging Face Spaces, use regex to match any .hf.space domain
|
| 74 |
# Note: If deploying to HF Spaces, add your Space URL to CORS_ORIGINS env var
|
| 75 |
-
# Example: CORS_ORIGINS=https://your-username-
|
| 76 |
|
| 77 |
app.add_middleware(
|
| 78 |
CORSMiddleware,
|
|
@@ -315,9 +315,9 @@ class TestingMatrixResponse(BaseModel):
|
|
| 315 |
async def api_info():
|
| 316 |
"""API info endpoint."""
|
| 317 |
return {
|
| 318 |
-
"name": "
|
| 319 |
"version": "2.0.0",
|
| 320 |
-
"description": "Generate high-converting ads using Angle × Concept matrix system",
|
| 321 |
"endpoints": {
|
| 322 |
"POST /generate": "Generate single ad (original mode)",
|
| 323 |
"POST /generate/batch": "Generate multiple ads (original mode)",
|
|
|
|
| 1 |
"""
|
| 2 |
+
PsyAdGenesis - FastAPI Application
|
| 3 |
+
Design ads that stop the scroll. Generate high-converting ad creatives for Home Insurance and GLP-1 niches
|
| 4 |
Saves all ads to Neon PostgreSQL database with image URLs
|
| 5 |
"""
|
| 6 |
|
|
|
|
| 40 |
async def lifespan(app: FastAPI):
|
| 41 |
"""Startup and shutdown events."""
|
| 42 |
# Startup: Connect to database
|
| 43 |
+
print("Starting PsyAdGenesis...")
|
| 44 |
await db_service.connect()
|
| 45 |
yield
|
| 46 |
# Shutdown: Disconnect from database
|
|
|
|
| 50 |
|
| 51 |
# Create FastAPI app with lifespan for database connection
|
| 52 |
app = FastAPI(
|
| 53 |
+
title="PsyAdGenesis",
|
| 54 |
+
description="Design ads that stop the scroll. Generate high-converting ad creatives for Home Insurance and GLP-1 niches using psychological triggers and AI-powered image generation.",
|
| 55 |
version="2.0.0",
|
| 56 |
lifespan=lifespan,
|
| 57 |
)
|
|
|
|
| 72 |
|
| 73 |
# For Hugging Face Spaces, use regex to match any .hf.space domain
|
| 74 |
# Note: If deploying to HF Spaces, add your Space URL to CORS_ORIGINS env var
|
| 75 |
+
# Example: CORS_ORIGINS=https://your-username-psyadgenesis.hf.space
|
| 76 |
|
| 77 |
app.add_middleware(
|
| 78 |
CORSMiddleware,
|
|
|
|
| 315 |
async def api_info():
|
| 316 |
"""API info endpoint."""
|
| 317 |
return {
|
| 318 |
+
"name": "PsyAdGenesis",
|
| 319 |
"version": "2.0.0",
|
| 320 |
+
"description": "Design ads that stop the scroll. Generate high-converting ads using Angle × Concept matrix system",
|
| 321 |
"endpoints": {
|
| 322 |
"POST /generate": "Generate single ad (original mode)",
|
| 323 |
"POST /generate/batch": "Generate multiple ads (original mode)",
|
services/database.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
Database Service for
|
| 3 |
Handles MongoDB connection and CRUD operations
|
| 4 |
"""
|
| 5 |
|
|
|
|
| 1 |
"""
|
| 2 |
+
Database Service for PsyAdGenesis
|
| 3 |
Handles MongoDB connection and CRUD operations
|
| 4 |
"""
|
| 5 |
|
services/generator.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""
|
| 2 |
Main Ad Generator Service
|
| 3 |
Combines LLM + Image generation with maximum randomization for variety
|
| 4 |
-
Uses professional prompting techniques
|
| 5 |
Saves ad creatives to Neon database with image URLs
|
| 6 |
"""
|
| 7 |
|
|
|
|
| 1 |
"""
|
| 2 |
Main Ad Generator Service
|
| 3 |
Combines LLM + Image generation with maximum randomization for variety
|
| 4 |
+
Uses professional prompting techniques for PsyAdGenesis
|
| 5 |
Saves ad creatives to Neon database with image URLs
|
| 6 |
"""
|
| 7 |
|
services/image.py
CHANGED
|
@@ -19,7 +19,7 @@ from openai import OpenAI
|
|
| 19 |
from config import settings
|
| 20 |
|
| 21 |
|
| 22 |
-
# Model registry
|
| 23 |
MODEL_REGISTRY: Dict[str, Dict[str, Any]] = {
|
| 24 |
"nano-banana": {
|
| 25 |
"id": "google/nano-banana",
|
|
|
|
| 19 |
from config import settings
|
| 20 |
|
| 21 |
|
| 22 |
+
# Model registry for PsyAdGenesis
|
| 23 |
MODEL_REGISTRY: Dict[str, Dict[str, Any]] = {
|
| 24 |
"nano-banana": {
|
| 25 |
"id": "google/nano-banana",
|
services/r2_storage.py
CHANGED
|
@@ -45,7 +45,7 @@ class R2StorageService:
|
|
| 45 |
region_name='auto', # R2 doesn't use regions
|
| 46 |
)
|
| 47 |
self.bucket_name = settings.r2_bucket_name
|
| 48 |
-
self.folder = "
|
| 49 |
|
| 50 |
def upload_image(
|
| 51 |
self,
|
|
|
|
| 45 |
region_name='auto', # R2 doesn't use regions
|
| 46 |
)
|
| 47 |
self.bucket_name = settings.r2_bucket_name
|
| 48 |
+
self.folder = "psyadgenesis"
|
| 49 |
|
| 50 |
def upload_image(
|
| 51 |
self,
|