sushilideaclan01 commited on
Commit
e026dc9
·
1 Parent(s): 0c08b0b

UPDATED GITIGNORE

Browse files
.gitignore CHANGED
@@ -10,7 +10,8 @@ dist/
10
  downloads/
11
  eggs/
12
  .eggs/
13
- lib/
 
14
  lib64/
15
  parts/
16
  sdist/
 
10
  downloads/
11
  eggs/
12
  .eggs/
13
+ # Python lib folders (not frontend/lib)
14
+ /lib/
15
  lib64/
16
  parts/
17
  sdist/
frontend/lib/api/client.ts ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from "axios";
2
+ import { toast } from "react-hot-toast";
3
+
4
+ // Use relative URL if NEXT_PUBLIC_API_URL is empty (same domain deployment)
5
+ // Otherwise use the provided URL or default to localhost
6
+ const API_URL = process.env.NEXT_PUBLIC_API_URL === ""
7
+ ? "" // Empty string means use relative URLs (same domain)
8
+ : (process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000");
9
+
10
+ // Create axios instance
11
+ const apiClient: AxiosInstance = axios.create({
12
+ baseURL: API_URL,
13
+ timeout: 300000, // 5 minutes for long-running operations
14
+ headers: {
15
+ "Content-Type": "application/json",
16
+ },
17
+ });
18
+
19
+ // Request interceptor
20
+ apiClient.interceptors.request.use(
21
+ (config: InternalAxiosRequestConfig) => {
22
+ // Add auth token if available
23
+ if (typeof window !== "undefined") {
24
+ const authStorage = localStorage.getItem("auth-storage");
25
+ if (authStorage) {
26
+ try {
27
+ const auth = JSON.parse(authStorage);
28
+ if (auth.state?.token) {
29
+ config.headers.Authorization = `Bearer ${auth.state.token}`;
30
+ }
31
+ } catch (e) {
32
+ // Ignore parsing errors
33
+ }
34
+ }
35
+ }
36
+ return config;
37
+ },
38
+ (error) => {
39
+ return Promise.reject(error);
40
+ }
41
+ );
42
+
43
+ // Response interceptor for error handling
44
+ apiClient.interceptors.response.use(
45
+ (response) => response,
46
+ (error: AxiosError) => {
47
+ if (error.response) {
48
+ // Server responded with error status
49
+ const status = error.response.status;
50
+ const message = (error.response.data as any)?.detail || error.message || "An error occurred";
51
+
52
+ // Handle 401 Unauthorized - redirect to login
53
+ if (status === 401) {
54
+ if (typeof window !== "undefined" && !window.location.pathname.includes("/login")) {
55
+ // Clear auth storage
56
+ localStorage.removeItem("auth-storage");
57
+ // Redirect to login
58
+ window.location.href = "/login";
59
+ }
60
+ }
61
+
62
+ // Don't show toast for 404s (handled in components) or 401s (handled above)
63
+ if (status !== 404 && status !== 401) {
64
+ toast.error(message);
65
+ }
66
+ } else if (error.request) {
67
+ // Request made but no response
68
+ toast.error("Network error. Please check your connection.");
69
+ } else {
70
+ // Something else happened
71
+ toast.error("An unexpected error occurred");
72
+ }
73
+
74
+ return Promise.reject(error);
75
+ }
76
+ );
77
+
78
+ export default apiClient;
frontend/lib/api/endpoints.ts ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import apiClient from "./client";
2
+ import type {
3
+ GenerateResponse,
4
+ BatchResponse,
5
+ MatrixGenerateResponse,
6
+ TestingMatrixResponse,
7
+ AnglesResponse,
8
+ ConceptsResponse,
9
+ CompatibleConceptsResponse,
10
+ AngleInfo,
11
+ ConceptInfo,
12
+ DbStatsResponse,
13
+ AdsListResponse,
14
+ AdCreativeDB,
15
+ HealthResponse,
16
+ ApiRootResponse,
17
+ ImageCorrectResponse,
18
+ LoginResponse,
19
+ Niche,
20
+ } from "../../types/api";
21
+
22
+ // Health & Info
23
+ export const getHealth = async (): Promise<HealthResponse> => {
24
+ const response = await apiClient.get<HealthResponse>("/health");
25
+ return response.data;
26
+ };
27
+
28
+ export const getApiInfo = async (): Promise<ApiRootResponse> => {
29
+ const response = await apiClient.get<ApiRootResponse>("/");
30
+ return response.data;
31
+ };
32
+
33
+ // Generation Endpoints
34
+ export const generateAd = async (params: {
35
+ niche: Niche;
36
+ num_images: number;
37
+ image_model?: string | null;
38
+ }): Promise<GenerateResponse> => {
39
+ const response = await apiClient.post<GenerateResponse>("/generate", params);
40
+ return response.data;
41
+ };
42
+
43
+ export const generateBatch = async (params: {
44
+ niche: Niche;
45
+ count: number;
46
+ images_per_ad: number;
47
+ image_model?: string | null;
48
+ }): Promise<BatchResponse> => {
49
+ const response = await apiClient.post<BatchResponse>("/generate/batch", params);
50
+ return response.data;
51
+ };
52
+
53
+ // Matrix Endpoints
54
+ export const generateMatrixAd = async (params: {
55
+ niche: Niche;
56
+ angle_key?: string | null;
57
+ concept_key?: string | null;
58
+ num_images: number;
59
+ image_model?: string | null;
60
+ }): Promise<MatrixGenerateResponse> => {
61
+ const response = await apiClient.post<MatrixGenerateResponse>("/matrix/generate", params);
62
+ return response.data;
63
+ };
64
+
65
+ // Extensive Endpoint
66
+ export const generateExtensiveAd = async (params: {
67
+ niche: Niche;
68
+ target_audience: string;
69
+ offer: string;
70
+ num_images?: number;
71
+ image_model?: string | null;
72
+ num_strategies?: number;
73
+ }): Promise<GenerateResponse> => {
74
+ const response = await apiClient.post<GenerateResponse>("/extensive/generate", params);
75
+ return response.data;
76
+ };
77
+
78
+ export const generateTestingMatrix = async (params: {
79
+ niche: Niche;
80
+ angle_count: number;
81
+ concept_count: number;
82
+ strategy: "balanced" | "top_performers" | "diverse";
83
+ }): Promise<TestingMatrixResponse> => {
84
+ const response = await apiClient.post<TestingMatrixResponse>("/matrix/testing", params);
85
+ return response.data;
86
+ };
87
+
88
+ export const getAllAngles = async (): Promise<AnglesResponse> => {
89
+ const response = await apiClient.get<AnglesResponse>("/matrix/angles");
90
+ return response.data;
91
+ };
92
+
93
+ export const getAllConcepts = async (): Promise<ConceptsResponse> => {
94
+ const response = await apiClient.get<ConceptsResponse>("/matrix/concepts");
95
+ return response.data;
96
+ };
97
+
98
+ export const getAngle = async (angleKey: string): Promise<AngleInfo> => {
99
+ const response = await apiClient.get<AngleInfo>(`/matrix/angle/${angleKey}`);
100
+ return response.data;
101
+ };
102
+
103
+ export const getConcept = async (conceptKey: string): Promise<ConceptInfo> => {
104
+ const response = await apiClient.get<ConceptInfo>(`/matrix/concept/${conceptKey}`);
105
+ return response.data;
106
+ };
107
+
108
+ export const getCompatibleConcepts = async (angleKey: string): Promise<CompatibleConceptsResponse> => {
109
+ const response = await apiClient.get<CompatibleConceptsResponse>(`/matrix/compatible/${angleKey}`);
110
+ return response.data;
111
+ };
112
+
113
+ // Database Endpoints
114
+ export const getDbStats = async (): Promise<DbStatsResponse> => {
115
+ const response = await apiClient.get<DbStatsResponse>("/db/stats");
116
+ return response.data;
117
+ };
118
+
119
+ export const listAds = async (params?: {
120
+ niche?: string | null;
121
+ generation_method?: string | null;
122
+ limit?: number;
123
+ offset?: number;
124
+ }): Promise<AdsListResponse> => {
125
+ const response = await apiClient.get<AdsListResponse>("/db/ads", { params });
126
+ return response.data;
127
+ };
128
+
129
+ export const getAd = async (adId: string): Promise<AdCreativeDB> => {
130
+ const response = await apiClient.get<AdCreativeDB>(`/db/ad/${adId}`);
131
+ return response.data;
132
+ };
133
+
134
+ export const deleteAd = async (adId: string): Promise<{ success: boolean; deleted_id: string }> => {
135
+ const response = await apiClient.delete<{ success: boolean; deleted_id: string }>(`/db/ad/${adId}`);
136
+ return response.data;
137
+ };
138
+
139
+ // Strategies
140
+ export const getStrategies = async (niche: Niche): Promise<any> => {
141
+ const response = await apiClient.get(`/strategies/${niche}`);
142
+ return response.data;
143
+ };
144
+
145
+ // Image Correction Endpoints
146
+ export const correctImage = async (params: {
147
+ image_id: string;
148
+ user_instructions?: string;
149
+ auto_analyze?: boolean;
150
+ }): Promise<ImageCorrectResponse> => {
151
+ const response = await apiClient.post<ImageCorrectResponse>("/api/correct", params);
152
+ return response.data;
153
+ };
154
+
155
+ // Auth Endpoints
156
+ export const login = async (username: string, password: string): Promise<LoginResponse> => {
157
+ const response = await apiClient.post<LoginResponse>("/auth/login", {
158
+ username,
159
+ password,
160
+ });
161
+ return response.data;
162
+ };
frontend/lib/constants/models.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Available image generation models
3
+ * These match the MODEL_REGISTRY in services/image.py
4
+ */
5
+ export const IMAGE_MODELS = [
6
+ { value: "", label: "Default (z-image-turbo)" },
7
+ { value: "z-image-turbo", label: "Z-Image Turbo (Fast & High Quality)" },
8
+ { value: "nano-banana", label: "Nano Banana (Google)" },
9
+ { value: "nano-banana-pro", label: "Nano Banana Pro (Google)" },
10
+ { value: "imagen-4", label: "Imagen-4 (Google)" },
11
+ { value: "imagen-4-ultra", label: "Imagen-4 Ultra (Google)" },
12
+ { value: "recraft-v3", label: "Recraft V3" },
13
+ { value: "ideogram-v3", label: "Ideogram V3" },
14
+ { value: "photon", label: "Photon (Luma)" },
15
+ { value: "seedream-3", label: "Seedream-3 (ByteDance)" },
16
+ ] as const;
17
+
18
+ export type ImageModel = typeof IMAGE_MODELS[number]["value"];
frontend/lib/utils/cn.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
frontend/lib/utils/export.ts ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Export utilities for downloading/exporting ads
2
+
3
+ export const downloadImage = async (url: string | null | undefined, filename: string | null | undefined): Promise<void> => {
4
+ if (!url) {
5
+ throw new Error("No image URL provided");
6
+ }
7
+
8
+ try {
9
+ const response = await fetch(url);
10
+ const blob = await response.blob();
11
+ const blobUrl = window.URL.createObjectURL(blob);
12
+ const link = document.createElement("a");
13
+ link.href = blobUrl;
14
+ link.download = filename || "image.png";
15
+ document.body.appendChild(link);
16
+ link.click();
17
+ document.body.removeChild(link);
18
+ window.URL.revokeObjectURL(blobUrl);
19
+ } catch (error) {
20
+ console.error("Error downloading image:", error);
21
+ throw error;
22
+ }
23
+ };
24
+
25
+ export const copyToClipboard = async (text: string): Promise<void> => {
26
+ try {
27
+ await navigator.clipboard.writeText(text);
28
+ } catch (error) {
29
+ console.error("Error copying to clipboard:", error);
30
+ throw error;
31
+ }
32
+ };
33
+
34
+ export const exportAsJSON = (data: any, filename: string): void => {
35
+ const jsonString = JSON.stringify(data, null, 2);
36
+ const blob = new Blob([jsonString], { type: "application/json" });
37
+ const url = window.URL.createObjectURL(blob);
38
+ const link = document.createElement("a");
39
+ link.href = url;
40
+ link.download = filename;
41
+ document.body.appendChild(link);
42
+ link.click();
43
+ document.body.removeChild(link);
44
+ window.URL.revokeObjectURL(url);
45
+ };
46
+
47
+ export const exportAsCSV = (data: any[], filename: string): void => {
48
+ if (data.length === 0) return;
49
+
50
+ const headers = Object.keys(data[0]);
51
+ const csvRows = [
52
+ headers.join(","),
53
+ ...data.map((row) =>
54
+ headers.map((header) => {
55
+ const value = row[header];
56
+ // Escape commas and quotes in CSV
57
+ if (typeof value === "string" && (value.includes(",") || value.includes('"'))) {
58
+ return `"${value.replace(/"/g, '""')}"`;
59
+ }
60
+ return value ?? "";
61
+ }).join(",")
62
+ ),
63
+ ];
64
+
65
+ const csvString = csvRows.join("\n");
66
+ const blob = new Blob([csvString], { type: "text/csv" });
67
+ const url = window.URL.createObjectURL(blob);
68
+ const link = document.createElement("a");
69
+ link.href = url;
70
+ link.download = filename;
71
+ document.body.appendChild(link);
72
+ link.click();
73
+ document.body.removeChild(link);
74
+ window.URL.revokeObjectURL(url);
75
+ };
frontend/lib/utils/formatters.ts ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { format, formatDistanceToNow } from "date-fns";
2
+
3
+ export const formatDate = (dateString: string | null | undefined): string => {
4
+ if (!dateString) return "N/A";
5
+ try {
6
+ return format(new Date(dateString), "MMM d, yyyy 'at' h:mm a");
7
+ } catch {
8
+ return dateString;
9
+ }
10
+ };
11
+
12
+ export const formatRelativeDate = (dateString: string | null | undefined): string => {
13
+ if (!dateString) return "N/A";
14
+ try {
15
+ return formatDistanceToNow(new Date(dateString), { addSuffix: true });
16
+ } catch {
17
+ return dateString;
18
+ }
19
+ };
20
+
21
+ export const formatNiche = (niche: string): string => {
22
+ return niche
23
+ .split("_")
24
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
25
+ .join(" ");
26
+ };
27
+
28
+ export const formatGenerationMethod = (method: string | null | undefined): string => {
29
+ if (!method) return "Original";
30
+ if (method === "angle_concept_matrix") return "Matrix";
31
+ return method
32
+ .split("_")
33
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
34
+ .join(" ");
35
+ };
36
+
37
+ export const truncateText = (text: string, maxLength: number): string => {
38
+ if (text.length <= maxLength) return text;
39
+ return text.slice(0, maxLength) + "...";
40
+ };
41
+
42
+ export const getImageUrl = (imageUrl: string | null | undefined, filename: string | null | undefined): string | null => {
43
+ // Prefer R2/external URL if it's a valid HTTP/HTTPS URL (R2 URLs or Replicate URLs)
44
+ // This ensures R2 URLs are always used when available
45
+ if (imageUrl && typeof imageUrl === 'string' && (imageUrl.startsWith('http://') || imageUrl.startsWith('https://'))) {
46
+ return imageUrl;
47
+ }
48
+ // Fallback to local filename only if no external URL exists
49
+ if (filename) {
50
+ const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
51
+ return `${apiUrl}/images/${filename}`;
52
+ }
53
+ return null;
54
+ };
55
+
56
+ export const getImageUrlFallback = (imageUrl: string | null | undefined, filename: string | null | undefined): { primary: string | null; fallback: string | null } => {
57
+ // Primary: R2/external URL if it's a valid HTTP/HTTPS URL
58
+ // This ensures R2 URLs are always prioritized
59
+ const primary = (imageUrl && typeof imageUrl === 'string' && (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')))
60
+ ? imageUrl
61
+ : null;
62
+ // Fallback: local filename only if no external URL
63
+ const fallback = filename
64
+ ? `${process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"}/images/${filename}`
65
+ : null;
66
+ return { primary, fallback };
67
+ };
frontend/lib/utils/validators.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { z } from "zod";
2
+
3
+ export const generateAdSchema = z.object({
4
+ niche: z.enum(["home_insurance", "glp1"]),
5
+ num_images: z.number().min(1).max(10),
6
+ image_model: z.string().optional().nullable(),
7
+ });
8
+
9
+ export const generateBatchSchema = z.object({
10
+ niche: z.enum(["home_insurance", "glp1"]),
11
+ count: z.number().min(1).max(20),
12
+ images_per_ad: z.number().min(1).max(3),
13
+ image_model: z.string().optional().nullable(),
14
+ });
15
+
16
+ export const generateMatrixSchema = z.object({
17
+ niche: z.enum(["home_insurance", "glp1"]),
18
+ angle_key: z.string().optional().nullable(),
19
+ concept_key: z.string().optional().nullable(),
20
+ num_images: z.number().min(1).max(5),
21
+ image_model: z.string().optional().nullable(),
22
+ });
23
+
24
+ export const testingMatrixSchema = z.object({
25
+ niche: z.enum(["home_insurance", "glp1"]),
26
+ angle_count: z.number().min(1).max(10),
27
+ concept_count: z.number().min(1).max(10),
28
+ strategy: z.enum(["balanced", "top_performers", "diverse"]),
29
+ });