From 3c47539128fb6bf1e18f69b94f13186ac0f0df55 Mon Sep 17 00:00:00 2001 From: pycomet Date: Sun, 6 Jul 2025 04:44:19 +0100 Subject: [PATCH 1/2] feat: add dashboard link to navbar and error boundary component --- app/api/generate-thumbnail/route.ts | 210 ++++++ app/dashboard/page.tsx | 1002 +++++++++++++++++++++++++ components/error-boundary.tsx | 142 ++++ components/navbar/index.tsx | 8 + components/search/searchComponent.tsx | 22 +- config/site.ts | 21 +- lib/ai/huggingface.ts | 218 ++++++ lib/firebase/firebaseConfig.ts | 17 +- package.json | 2 + yarn.lock | 23 + 10 files changed, 1648 insertions(+), 17 deletions(-) create mode 100644 app/api/generate-thumbnail/route.ts create mode 100644 app/dashboard/page.tsx create mode 100644 components/error-boundary.tsx create mode 100644 lib/ai/huggingface.ts diff --git a/app/api/generate-thumbnail/route.ts b/app/api/generate-thumbnail/route.ts new file mode 100644 index 0000000..ba4195a --- /dev/null +++ b/app/api/generate-thumbnail/route.ts @@ -0,0 +1,210 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + generateThumbnail, + ThumbnailGenerationOptions, +} from "@/lib/ai/huggingface"; + +// Enhanced error response helper +function createErrorResponse(message: string, status: number = 500, type?: string) { + return NextResponse.json( + { + error: message, + type: type || "api", + success: false, + timestamp: new Date().toISOString(), + }, + { status } + ); +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { + prompt, + style = "tech", + model = "sdxl", + quality = "balanced", + userId, + } = body; + + // Enhanced input validation + if (!prompt || typeof prompt !== "string" || prompt.trim().length === 0) { + return createErrorResponse( + "Please enter a video description", + 400, + "validation" + ); + } + + if (prompt.length > 500) { + return createErrorResponse( + "Description must be less than 500 characters", + 400, + "validation" + ); + } + + if (prompt.trim().length < 5) { + return createErrorResponse( + "Description must be at least 5 characters long", + 400, + "validation" + ); + } + + // Validate model and quality + const validModels = ["sdxl", "flux", "realistic"]; + const validQualities = ["fast", "balanced", "high"]; + const validStyles = ["tech", "gaming", "tutorial", "lifestyle"]; + + if (!validModels.includes(model)) { + return createErrorResponse( + "Invalid AI model selected. Please choose a valid model.", + 400, + "validation" + ); + } + + if (!validQualities.includes(quality)) { + return createErrorResponse( + "Invalid quality setting selected. Please choose a valid quality level.", + 400, + "validation" + ); + } + + if (!validStyles.includes(style)) { + return createErrorResponse( + "Invalid style selected. Please choose a valid style.", + 400, + "validation" + ); + } + + // Check for API key + if (!process.env.HUGGINGFACE_API_KEY) { + return createErrorResponse( + "AI service is not configured. Please contact support.", + 500, + "api" + ); + } + + // Generate thumbnail with enhanced error handling + const options: ThumbnailGenerationOptions = { + prompt: prompt.trim(), + style, + model, + quality, + userId, + }; + + console.log("Generating thumbnail with options:", { + prompt: prompt.substring(0, 50) + "...", + style, + model, + quality, + userId: userId ? "***" : "none", + }); + + const result = await generateThumbnail(options); + + // Convert blob to base64 for response + const arrayBuffer = await result.imageBlob.arrayBuffer(); + const base64 = Buffer.from(arrayBuffer).toString("base64"); + const dataUrl = `data:image/png;base64,${base64}`; + + return NextResponse.json({ + success: true, + imageUrl: dataUrl, + prompt: result.prompt, + style: result.style, + model: result.model, + parameters: result.parameters, + timestamp: new Date().toISOString(), + }); + } catch (error) { + console.error("API Error:", error); + + // Enhanced error categorization + if (error instanceof Error) { + const errorMessage = error.message.toLowerCase(); + + // Network/connection errors + if (errorMessage.includes("fetch") || errorMessage.includes("network")) { + return createErrorResponse( + "Unable to connect to AI service. Please check your internet connection.", + 503, + "network" + ); + } + + // Rate limiting errors + if (errorMessage.includes("rate") || errorMessage.includes("limit") || errorMessage.includes("quota")) { + return createErrorResponse( + "Too many requests. Please wait a moment before trying again.", + 429, + "quota" + ); + } + + // Model-specific errors + if (errorMessage.includes("model") || errorMessage.includes("loading")) { + return createErrorResponse( + "AI model is currently unavailable. Try switching to a different model or wait a moment.", + 503, + "model" + ); + } + + // Authentication errors + if (errorMessage.includes("unauthorized") || errorMessage.includes("forbidden")) { + return createErrorResponse( + "AI service authentication failed. Please contact support.", + 401, + "api" + ); + } + + // Timeout errors + if (errorMessage.includes("timeout") || errorMessage.includes("aborted")) { + return createErrorResponse( + "Request timed out. Please try again with a shorter description.", + 408, + "network" + ); + } + + // Return the actual error message for debugging + return createErrorResponse( + `Generation failed: ${error.message}`, + 500, + "api" + ); + } + + // Fallback for unknown errors + return createErrorResponse( + "An unexpected error occurred. Please try again.", + 500, + "unknown" + ); + } +} + +export async function GET() { + return NextResponse.json({ + message: "PixelAI Thumbnail Generation API", + version: "1.0.0", + status: "online", + supportedStyles: ["tech", "gaming", "tutorial", "lifestyle"], + supportedModels: ["sdxl", "flux", "realistic"], + supportedQualities: ["fast", "balanced", "high"], + limits: { + maxPromptLength: 500, + minPromptLength: 5, + }, + timestamp: new Date().toISOString(), + }); +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000..5abee26 --- /dev/null +++ b/app/dashboard/page.tsx @@ -0,0 +1,1002 @@ +"use client"; +import { useState, useEffect, Suspense } from "react"; +import { + Button, + Input, + Card, + CardBody, + CardHeader, + Image, + Chip, + Divider, + Progress, + Badge, + Spinner, +} from "@nextui-org/react"; +import { PageLayout } from "@/components/layouts/pageLayout"; +import { useUser } from "@/contexts/userContext"; +import { useMessage } from "@/contexts/messageContext"; +import { title, subtitle, button } from "@/components/primitives"; +import { useRouter, useSearchParams } from "next/navigation"; +import { AnimatedDiv } from "@/components/motion"; +import { SearchIcon, RightArrowIcon } from "@/components/icons"; +import { motion } from "framer-motion"; +import { Cpu, Clock, AlertCircle, RefreshCw } from "lucide-react"; + +type ThumbnailStyle = "tech" | "gaming" | "tutorial" | "lifestyle"; + +interface GenerationResult { + imageUrl: string; + prompt: string; + style: string; + model: string; + parameters: { + steps: number; + guidance_scale: number; + negative_prompt: string; + }; + timestamp: string; +} + +// Error types for better error handling +interface ErrorInfo { + type: "validation" | "network" | "api" | "quota" | "model" | "unknown"; + message: string; + suggestion?: string; + retryable: boolean; +} + +// Error categorization helper +const categorizeError = (error: any): ErrorInfo => { + const errorMessage = error?.message || error?.toString() || "Unknown error"; + const errorLower = errorMessage.toLowerCase(); + + // Network errors + if ( + errorLower.includes("network") || + errorLower.includes("fetch") || + errorLower.includes("connection") + ) { + return { + type: "network", + message: "Unable to connect to our servers", + suggestion: "Please check your internet connection and try again", + retryable: true, + }; + } + + // API quota errors + if ( + errorLower.includes("quota") || + errorLower.includes("limit") || + errorLower.includes("rate") + ) { + return { + type: "quota", + message: "Generation limit reached", + suggestion: "Please try again in a few minutes or consider upgrading", + retryable: true, + }; + } + + // Model errors + if ( + errorLower.includes("model") || + errorLower.includes("unavailable") || + errorLower.includes("loading") + ) { + return { + type: "model", + message: "AI model is temporarily unavailable", + suggestion: "Try switching to a different model or wait a moment", + retryable: true, + }; + } + + // Validation errors + if ( + errorLower.includes("invalid") || + errorLower.includes("required") || + errorLower.includes("validation") + ) { + return { + type: "validation", + message: errorMessage, + suggestion: "Please check your input and try again", + retryable: false, + }; + } + + // API errors + if ( + errorLower.includes("api") || + errorLower.includes("server") || + errorLower.includes("500") + ) { + return { + type: "api", + message: "Server error occurred", + suggestion: "Our servers are experiencing issues. Please try again shortly", + retryable: true, + }; + } + + // Unknown errors + return { + type: "unknown", + message: "Something went wrong", + suggestion: "Please try again or contact support if the problem persists", + retryable: true, + }; +}; + +const styleOptions = [ + { + key: "tech", + label: "Tech Review", + description: "Modern, clean, professional", + gradient: "from-blue-500 to-cyan-500", + icon: "๐Ÿ’ป", + }, + { + key: "gaming", + label: "Gaming", + description: "Intense, dramatic, neon colors", + gradient: "from-purple-500 to-pink-500", + icon: "๐ŸŽฎ", + }, + { + key: "tutorial", + label: "Tutorial", + description: "Educational, clear, step-by-step", + gradient: "from-green-500 to-teal-500", + icon: "๐Ÿ“š", + }, + { + key: "lifestyle", + label: "Lifestyle", + description: "Warm, personal, authentic", + gradient: "from-orange-500 to-red-500", + icon: "โœจ", + }, +]; + +const modelOptions = [ + { + value: "sdxl", + label: "Stable Diffusion XL", + description: "Best balance of quality and speed", + icon: "โšก", + recommended: true, + }, + { + value: "flux", + label: "FLUX Schnell", + description: "Latest model with superior quality", + icon: "๐Ÿš€", + recommended: false, + }, + { + value: "realistic", + label: "Realistic SD", + description: "More photorealistic outputs", + icon: "๐Ÿ“ธ", + recommended: false, + }, +]; + +const qualityOptions = [ + { + value: "fast", + label: "Fast", + description: "Quick generation (~15s)", + icon: "โšก", + recommended: false, + }, + { + value: "balanced", + label: "Balanced", + description: "Good quality (~25s)", + icon: "โš–๏ธ", + recommended: true, + }, + { + value: "high", + label: "High", + description: "Best quality (~40s)", + icon: "๐Ÿ’Ž", + recommended: false, + }, +]; + +// Intelligent suggestion system - creates short, punchy suggestions +const generateSmartSuggestions = (input: string): string[] => { + const trimmedInput = input.trim().toLowerCase(); + + if (trimmedInput.length < 3) return []; + + // Keywords and patterns for different types of content + const patterns = { + tech: [ + "iphone", + "android", + "laptop", + "pc", + "review", + "unboxing", + "setup", + "build", + "coding", + "app", + "software", + "hardware", + "gadget", + "phone", + "computer", + "tech", + "ai", + "robot", + ], + gaming: [ + "game", + "gaming", + "play", + "stream", + "twitch", + "fps", + "rpg", + "minecraft", + "fortnite", + "valorant", + "league", + "cod", + "setup", + "build", + "pc gaming", + "console", + ], + tutorial: [ + "how to", + "tutorial", + "guide", + "learn", + "teach", + "explain", + "step", + "beginner", + "advanced", + "tips", + "tricks", + "method", + "way to", + ], + lifestyle: [ + "morning", + "routine", + "day in", + "vlog", + "life", + "travel", + "food", + "cooking", + "workout", + "fitness", + "home", + "room", + "outfit", + "style", + ], + entertainment: [ + "funny", + "comedy", + "react", + "reaction", + "challenge", + "prank", + "story", + "storytime", + "drama", + "expose", + "truth", + ], + }; + + // Detect content type + let contentType = "general"; + + for (const [type, keywords] of Object.entries(patterns)) { + const matches = keywords.filter((keyword) => + trimmedInput.includes(keyword) + ); + if (matches.length > 0) { + contentType = type; + break; + } + } + + const suggestions: string[] = []; + + // Generate shorter, punchier suggestions based on content type + switch (contentType) { + case "tech": + if (trimmedInput.includes("iphone") || trimmedInput.includes("phone")) { + suggestions.push( + "iPhone camera test results", + "Phone battery life review", + "iPhone vs Android comparison" + ); + } else if (trimmedInput.includes("review")) { + suggestions.push( + "Tech review honest verdict", + "Gadget review pros cons", + "Review after 30 days" + ); + } else if (trimmedInput.includes("unbox")) { + suggestions.push( + "Unboxing first impressions setup", + "Unboxing hidden features revealed", + "Unboxing build quality test" + ); + } else { + suggestions.push( + "Tech tutorial complete guide", + "Tech breakdown analysis", + "Tech secrets exposed" + ); + } + break; + + case "gaming": + if (trimmedInput.includes("setup") || trimmedInput.includes("build")) { + suggestions.push( + "Gaming setup complete guide", + "Budget gaming build guide", + "Max FPS gaming setup" + ); + } else if (trimmedInput.includes("review")) { + suggestions.push( + "Game review honest verdict", + "Gaming gear review test", + "Game worth buying" + ); + } else { + suggestions.push( + "Gaming highlights epic moments", + "Gaming pro strategies tips", + "Gaming skills showcase" + ); + } + break; + + case "tutorial": + suggestions.push( + "Complete beginner tutorial guide", + "Advanced techniques made simple", + "Expert secrets tutorial" + ); + break; + + case "lifestyle": + if (trimmedInput.includes("routine")) { + suggestions.push( + "Morning routine that works", + "Life routine for success", + "Routine secrets revealed" + ); + } else if ( + trimmedInput.includes("vlog") || + trimmedInput.includes("day") + ) { + suggestions.push( + "Day in life vlog", + "Behind scenes moments", + "Real life unfiltered" + ); + } else { + suggestions.push( + "Lifestyle transformation results", + "Life journey lessons", + "Lifestyle experience insights" + ); + } + break; + + case "entertainment": + suggestions.push( + "Funny moments hilarious reactions", + "Reaction video genuine emotions", + "Entertainment content results" + ); + break; + + default: + // Generic suggestions that work for any content + suggestions.push( + "Complete step by step", + "Honest review results", + "Detailed analysis secrets" + ); + } + + // Return unique suggestions, max 3 + return suggestions + .map((s) => s.replace(/\s+/g, " ").trim()) + .filter((s, index, arr) => arr.indexOf(s) === index) + .slice(0, 3); +}; + +function DashboardContent() { + const { user, loading: userLoading } = useUser(); + const { message } = useMessage(); + const router = useRouter(); + const searchParams = useSearchParams(); + + const [prompt, setPrompt] = useState(""); + const [style, setStyle] = useState("tech"); + const [model, setModel] = useState("sdxl"); + const [quality, setQuality] = useState("balanced"); + const [loading, setLoading] = useState(false); + const [progress, setProgress] = useState(0); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [retryCount, setRetryCount] = useState(0); + const [suggestions, setSuggestions] = useState([]); + + // Handle search parameters + useEffect(() => { + const searchPrompt = searchParams.get("prompt"); + if (searchPrompt) { + setPrompt(searchPrompt); + } + }, [searchParams]); + + // Generate suggestions based on user input + useEffect(() => { + if (prompt.trim().length >= 3) { + const newSuggestions = generateSmartSuggestions(prompt); + setSuggestions(newSuggestions); + } else { + setSuggestions([]); + } + }, [prompt]); + + // Simulate progress during generation + useEffect(() => { + if (loading) { + setProgress(0); + const interval = setInterval(() => { + setProgress((prev) => { + if (prev >= 90) return prev; + return prev + Math.random() * 15; + }); + }, 500); + return () => clearInterval(interval); + } + }, [loading]); + + // Show toast notifications for errors + useEffect(() => { + if (error) { + message(error.message, "error"); + } + }, [error, message]); + + // Redirect if not authenticated (unless auth is disabled) + const authDisabled = process.env.NEXT_PUBLIC_DISABLE_AUTH === "true"; + if (!authDisabled && !userLoading && !user) { + router.push("/"); + return null; + } + + const handleGenerate = async () => { + if (!prompt.trim()) { + const validationError = { + type: "validation" as const, + message: "Please enter a video description", + retryable: false + }; + setError(validationError); + return; + } + + setLoading(true); + setError(null); + setResult(null); + setProgress(0); + + // Simulate progress updates + const progressInterval = setInterval(() => { + setProgress((prev) => Math.min(prev + 10, 90)); + }, 800); + + try { + const userId = authDisabled ? "demo-user" : user?.uid; + const response = await fetch("/api/generate-thumbnail", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + prompt, + style, + model, + quality, + userId, + }), + }); + + clearInterval(progressInterval); + setProgress(100); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + if (data.success) { + setResult(data); + message("Thumbnail generated successfully!", "success"); + setRetryCount(0); + } else { + const errorInfo = categorizeError(data.error); + setError(errorInfo); + } + } catch (err) { + clearInterval(progressInterval); + const errorInfo = categorizeError(err); + setError(errorInfo); + console.error("Generation error:", err); + } finally { + setLoading(false); + setTimeout(() => setProgress(0), 2000); + } + }; + + const handleRetry = () => { + setRetryCount((prev) => prev + 1); + setError(null); + setTimeout(() => { + handleGenerate(); + }, 1000); + }; + + const handleSuggestionClick = (suggestion: string) => { + setPrompt(suggestion); + setSuggestions([]); + setError(null); + }; + + if (userLoading) { + return ( + +
+ +
+
+ ); + } + + return ( + +
+ {/* Header Section */} + +

Create Your Perfect 

+

+ YouTube Thumbnail +

+

+ Transform your video ideas into eye-catching thumbnails that boost + clicks and views +

+
+ +
+ {/* Main Generation Section */} +
+ {/* Input Section */} + + + +
+

+ Describe Your Video +

+

+ Tell us what your video is about and we'll create the + perfect thumbnail +

+
+ + setPrompt(e.target.value)} + maxLength={500} + description={`${prompt.length}/500 characters`} + startContent={ + + } + classNames={{ + inputWrapper: [ + "shadow-md", + "bg-default-200/50", + "dark:bg-default/60", + "backdrop-blur-xl", + "hover:bg-default-200/70", + "dark:hover:bg-default/70", + "group-data-[focus=true]:bg-default-200/50", + "dark:group-data-[focus=true]:bg-default/60", + "border-2", + "border-transparent", + "group-data-[focus=true]:border-primary/50", + ], + }} + /> + + {/* Intelligent Suggestions */} + {suggestions.length > 0 && ( +
+

+ Or try these suggestions: +

+
+ {suggestions.map((suggestion, index) => ( + handleSuggestionClick(suggestion)} + title={suggestion} + > + + {suggestion} + + + ))} +
+

+ Click a suggestion to use it, or keep typing for more ideas +

+
+ )} +
+
+
+ + {/* Style Selection */} + + + +
+

Choose Your Style

+

+ Select the aesthetic that matches your content +

+
+ +
+ {styleOptions.map((option) => ( + setStyle(option.key as ThumbnailStyle)} + > + +
+ {option.icon} +
+

+ {option.label} +

+

+ {option.description} +

+
+
+ ))} +
+
+
+
+ + {/* Model and Quality Settings */} + + + +
+

+ Advanced Settings +

+

+ Fine-tune your generation parameters +

+
+ +
+ {/* Model Selection */} +
+
+ + + AI Model + +
+
+ {modelOptions.map((option) => ( + + setModel(option.value)} + > + +
+
+
+ + {option.label} + + {option.recommended && ( + + Recommended + + )} +
+

+ {option.description} +

+
+
{option.icon}
+
+
+
+
+ ))} +
+
+ + {/* Quality Selection */} +
+
+ + + Quality + +
+
+ {qualityOptions.map((option) => ( + + setQuality(option.value)} + > + +
+
+
+ + {option.label} + + {option.recommended && ( + + Balanced + + )} +
+

+ {option.description} +

+
+
{option.icon}
+
+
+
+
+ ))} +
+
+
+
+
+
+ + {/* Generate Button */} + + + + + {/* Progress Bar */} + {loading && ( + + + +
+
+

Generating...

+

+ {Math.round(progress)}% +

+
+ +
+ Using {modelOptions.find((m) => m.value === model)?.label} โ€ข {qualityOptions.find((q) => q.value === quality)?.label} quality +
+
+
+
+
+ )} + + {/* Enhanced Error Display */} + {error && ( + + + +
+ +
+
+ {error.message} +
+ {error.suggestion && ( +
+ {error.suggestion} +
+ )} + {retryCount > 0 && ( +
+ Retry attempt: {retryCount} +
+ )} +
+ {error.retryable && ( + + )} +
+
+
+
+ )} +
+ + {/* Results Panel */} +
+
+ + +

Preview

+
+ + {result ? ( +
+
+ Generated thumbnail +
+ + + +
+
+ Model: + + {modelOptions.find((m) => m.value === model)?.label} + +
+
+ Style: + + {styleOptions.find((s) => s.key === style)?.label} + +
+
+ + Parameters: + + + {result.parameters.steps} steps, {result.parameters.guidance_scale} guidance + +
+
+ + +
+ ) : ( +
+
+ ๐ŸŽจ +
+

+ Your generated thumbnail will appear here +

+
+ )} +
+
+
+
+
+
+
+ ); +} + +export default function Dashboard() { + return ( + Loading...}> + + + ); +} diff --git a/components/error-boundary.tsx b/components/error-boundary.tsx new file mode 100644 index 0000000..8a854b6 --- /dev/null +++ b/components/error-boundary.tsx @@ -0,0 +1,142 @@ +"use client"; + +import React from "react"; +import { Card, CardBody, Button } from "@nextui-org/react"; +import { AlertCircle, RefreshCw } from "lucide-react"; + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; + errorInfo: string | null; +} + +interface ErrorBoundaryProps { + children: React.ReactNode; + fallback?: React.ComponentType<{ error: Error; reset: () => void }>; +} + +export class ErrorBoundary extends React.Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { + hasError: false, + error: null, + errorInfo: null, + }; + } + + static getDerivedStateFromError(error: Error): Partial { + return { + hasError: true, + error, + }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error("Error Boundary caught an error:", error, errorInfo); + this.setState({ + error, + errorInfo: errorInfo.componentStack, + }); + } + + handleReset = () => { + this.setState({ + hasError: false, + error: null, + errorInfo: null, + }); + }; + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + const FallbackComponent = this.props.fallback; + return ( + + ); + } + + return ( +
+ + +
+ +
+
+

+ Something went wrong +

+

+ An unexpected error occurred while loading this page. +

+
+ + {process.env.NODE_ENV === "development" && this.state.error && ( +
+

+ {this.state.error.message} +

+
+ )} + +
+ + +
+
+
+
+
+
+ ); + } + + return this.props.children; + } +} + +// Hook version for functional components +export function useErrorBoundary() { + const [error, setError] = React.useState(null); + + const resetError = React.useCallback(() => { + setError(null); + }, []); + + const captureError = React.useCallback((error: Error) => { + setError(error); + }, []); + + React.useEffect(() => { + if (error) { + throw error; + } + }, [error]); + + return { captureError, resetError }; +} + +export default ErrorBoundary; \ No newline at end of file diff --git a/components/navbar/index.tsx b/components/navbar/index.tsx index 47cc39f..f052de0 100644 --- a/components/navbar/index.tsx +++ b/components/navbar/index.tsx @@ -49,6 +49,14 @@ export const Navbar = () => { + + Dashboard + Profile diff --git a/components/search/searchComponent.tsx b/components/search/searchComponent.tsx index 6e0ac2c..e6ddbbb 100644 --- a/components/search/searchComponent.tsx +++ b/components/search/searchComponent.tsx @@ -8,14 +8,34 @@ import { AnimatedDiv, AnimatedList } from "@/components/motion"; import { SearchIcon } from "@/components/icons"; import { button } from "@/components/primitives"; import { siteConfig } from "@/config/site"; +import { useRouter } from "next/navigation"; +import { useUser } from "@/contexts/userContext"; export const SearchComponent: React.FC = () => { const [inputValue, setInputValue] = useState(""); + const router = useRouter(); + const { user, toggleLogin } = useUser(); const handleInputChange = (event: React.ChangeEvent) => { setInputValue(event.target.value); }; + const handleGenerate = () => { + const authDisabled = process.env.NEXT_PUBLIC_DISABLE_AUTH === "true"; + + if (!authDisabled && !user) { + toggleLogin(); + return; + } + + // Navigate to dashboard with the search query + const searchParams = new URLSearchParams(); + if (inputValue.trim()) { + searchParams.set("prompt", inputValue.trim()); + } + router.push(`/dashboard?${searchParams.toString()}`); + }; + return ( { } endContent={ - diff --git a/config/site.ts b/config/site.ts index b75c348..cd4a88b 100644 --- a/config/site.ts +++ b/config/site.ts @@ -5,14 +5,14 @@ export const siteConfig = { description: "Using AI to create engaging thumbnails for online content using advanced language models and image processing. Streamlines the thumbnail creation workflow and enhances visual presentation to boost engagement.", navItems: [ - // { - // label: "Home", - // href: "/", - // }, - // { - // label: "Pricing", - // href: "/pricing", - // }, + { + label: "Home", + href: "/", + }, + { + label: "Dashboard", + href: "/dashboard", + }, ], navMenuItems: [ { @@ -20,6 +20,11 @@ export const siteConfig = { href: "/", protected: false, }, + { + label: "Dashboard", + href: "/dashboard", + protected: true, + }, { label: "About Us", href: "/about", diff --git a/lib/ai/huggingface.ts b/lib/ai/huggingface.ts new file mode 100644 index 0000000..01e8229 --- /dev/null +++ b/lib/ai/huggingface.ts @@ -0,0 +1,218 @@ +import { HfInference } from "@huggingface/inference"; + +const hf = new HfInference(process.env.HUGGINGFACE_API_KEY); + +export interface ThumbnailGenerationOptions { + prompt: string; + style?: "tech" | "gaming" | "tutorial" | "lifestyle"; + userId?: string; + model?: "sdxl" | "flux" | "realistic"; + quality?: "fast" | "balanced" | "high"; +} + +export interface ThumbnailResult { + imageBlob: Blob; + prompt: string; + style: string; + model: string; + parameters: GenerationParameters; +} + +interface GenerationParameters { + model: string; + steps: number; + guidance_scale: number; + negative_prompt: string; + scheduler?: string; + width: number; + height: number; +} + +// Available models with their strengths +const models = { + sdxl: { + id: "stabilityai/stable-diffusion-xl-base-1.0", + name: "Stable Diffusion XL", + description: "Best balance of quality and speed", + recommended: true, + }, + flux: { + id: "black-forest-labs/FLUX.1-schnell", + name: "FLUX Schnell", + description: "Latest model with superior quality", + recommended: false, // Often not available in free tier + }, + realistic: { + id: "stabilityai/stable-diffusion-2-1", + name: "Stable Diffusion 2.1", + description: "More realistic outputs", + recommended: false, + }, +}; + +// Quality presets that significantly impact output +const qualityPresets = { + fast: { + steps: 15, + guidance_scale: 7.0, + description: "Quick generation (~10-15s)", + }, + balanced: { + steps: 25, + guidance_scale: 8.5, + description: "Good quality (~20-25s)", + }, + high: { + steps: 40, + guidance_scale: 10.0, + description: "Best quality (~30-40s)", + }, +}; + +// YouTube-optimized style prompts with negative prompts +const stylePrompts = { + tech: { + positive: + "modern tech aesthetic, clean minimalist design, blue and white colors, professional quality, sharp focus, high contrast, bold typography, sleek gadgets, futuristic elements", + negative: + "blurry, low quality, pixelated, oversaturated, cluttered, messy text, poor lighting, amateur, grainy, distorted", + basePrompt: "YouTube tech review thumbnail:", + }, + gaming: { + positive: + "intense gaming aesthetic, dramatic lighting, neon colors, action-packed, high energy, bold graphics, gaming setup, RGB lighting, epic atmosphere, dynamic composition", + negative: + "boring, static, dull colors, poor composition, blurry, low contrast, amateur lighting, pixelated, distorted", + basePrompt: "YouTube gaming thumbnail:", + }, + tutorial: { + positive: + "educational style, clear typography, step-by-step visual, professional presentation, organized layout, instructional design, clean background, easy to read text", + negative: + "confusing layout, cluttered, poor readability, blurry text, amateur design, low quality, messy composition", + basePrompt: "YouTube tutorial thumbnail:", + }, + lifestyle: { + positive: + "warm personal aesthetic, authentic feel, natural lighting, lifestyle photography, cozy atmosphere, relatable, human-centered, soft colors, inviting mood", + negative: + "artificial, fake, oversaturated, poor lighting, low quality, blurry, unprofessional, cluttered background", + basePrompt: "YouTube lifestyle thumbnail:", + }, +}; + +// Universal negative prompt for all thumbnails +const universalNegativePrompt = ` +low quality, blurry, pixelated, distorted, amateur, poor composition, +bad lighting, oversaturated, undersaturated, text too small, unreadable text, +cluttered, messy, unprofessional, low resolution, jpeg artifacts, +watermark, signature, copyright, nsfw, inappropriate content +`.trim(); + +export async function generateThumbnail( + options: ThumbnailGenerationOptions +): Promise { + const { + prompt, + style = "tech", + model = "sdxl", + quality = "balanced", + } = options; + + // Get model and quality settings + const selectedModel = models[model]; + const qualitySettings = qualityPresets[quality]; + const styleConfig = stylePrompts[style]; + + // Build optimized prompt for YouTube thumbnails + const optimizedPrompt = ` +${styleConfig.basePrompt} ${prompt}, ${styleConfig.positive}, +YouTube thumbnail design, 16:9 aspect ratio, eye-catching, clickable, +high contrast, bold elements, professional quality, trending style, +perfect for YouTube, attention-grabbing, viral potential +`.trim(); + + // Combine negative prompts + const negativePrompt = `${styleConfig.negative}, ${universalNegativePrompt}`; + + // Generation parameters optimized for thumbnails + const parameters: GenerationParameters = { + model: selectedModel.id, + width: 1280, + height: 720, + steps: qualitySettings.steps, + guidance_scale: qualitySettings.guidance_scale, + negative_prompt: negativePrompt, + }; + + try { + console.log("Generating with parameters:", { + model: selectedModel.name, + quality, + style, + steps: parameters.steps, + guidance: parameters.guidance_scale, + }); + + const response = await hf.textToImage({ + model: selectedModel.id, + inputs: optimizedPrompt, + parameters: { + width: parameters.width, + height: parameters.height, + num_inference_steps: parameters.steps, + guidance_scale: parameters.guidance_scale, + negative_prompt: parameters.negative_prompt, + // Additional parameters for better quality + scheduler: "DPMSolverMultistepScheduler", // Better scheduler + use_karras_sigmas: true, // Better noise scheduling + clip_skip: 1, // For better prompt following + }, + }); + + return { + imageBlob: response as unknown as Blob, + prompt: optimizedPrompt, + style, + model: selectedModel.name, + parameters, + }; + } catch (error) { + console.error("Hugging Face API error:", error); + throw new Error( + `Failed to generate thumbnail: ${ + error instanceof Error ? error.message : "Unknown error" + }` + ); + } +} + +// Get available models for UI +export function getAvailableModels() { + return Object.entries(models).map(([key, model]) => ({ + key, + ...model, + })); +} + +// Get quality presets for UI +export function getQualityPresets() { + return Object.entries(qualityPresets).map(([key, preset]) => ({ + key, + ...preset, + })); +} + +export async function testConnection(): Promise { + try { + await hf.textToImage({ + model: models.sdxl.id, + inputs: "test connection", + parameters: { width: 512, height: 512, num_inference_steps: 1 }, + }); + return true; + } catch (error) { + console.error("Hugging Face connection test failed:", error); + return false; + } +} diff --git a/lib/firebase/firebaseConfig.ts b/lib/firebase/firebaseConfig.ts index 2601784..90d3e62 100644 --- a/lib/firebase/firebaseConfig.ts +++ b/lib/firebase/firebaseConfig.ts @@ -4,16 +4,17 @@ import { initializeApp } from "firebase/app"; import { getAuth } from "firebase/auth"; const firebaseConfig = { - apiKey: process.env.NEXT_FIREBASE_API_KEY, - authDomain: process.env.NEXT_FIREBASE_AUTH_DOMAIN, - projectId: process.env.NEXT_FIREBASE_PROJECT_ID, - storageBucket: process.env.NEXT_FIREBASE_STORAGE_BUCKET, - messagingSenderId: process.env.NEXT_FIREBASE_SENDER_ID, - appId: process.env.NEXT_FIREBASE_APP_ID, - measurementId: process.env.NEXT_FIREBASE_MEASUREMENT_ID, + apiKey: process.env.NEXT_FIREBASE_API_KEY || "demo-key", + authDomain: + process.env.NEXT_FIREBASE_AUTH_DOMAIN || "demo-project.firebaseapp.com", + projectId: process.env.NEXT_FIREBASE_PROJECT_ID || "demo-project", + storageBucket: + process.env.NEXT_FIREBASE_STORAGE_BUCKET || "demo-project.appspot.com", + messagingSenderId: process.env.NEXT_FIREBASE_SENDER_ID || "123456789", + appId: process.env.NEXT_FIREBASE_APP_ID || "1:123456789:web:abcdef", + measurementId: process.env.NEXT_FIREBASE_MEASUREMENT_ID || "G-ABCDEF", }; -console.log(process.env.NEXT_FIREBASE_API_KEY); // Initialize Firebase const app = initializeApp(firebaseConfig); diff --git a/package.json b/package.json index eeed28f..dffa2e0 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "@firebase/firestore": "^4.6.3", "@hookform/resolvers": "^3.6.0", + "@huggingface/inference": "^4.3.2", "@nextui-org/button": "^2.0.32", "@nextui-org/code": "^2.0.28", "@nextui-org/input": "^2.2.0", @@ -52,6 +53,7 @@ "firebase-admin": "^12.1.1", "framer-motion": "^11.2.10", "intl-messageformat": "^10.5.0", + "lucide-react": "^0.525.0", "next": "14.2.3", "next-themes": "^0.2.1", "postcss": "8.4.38", diff --git a/yarn.lock b/yarn.lock index 45b3f5c..09d5de0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -879,6 +879,24 @@ resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-3.6.0.tgz#71ae08acf7f7624fb24ea0505de00b9001a63687" integrity sha512-UBcpyOX3+RR+dNnqBd0lchXpoL8p4xC21XP8H6Meb8uve5Br1GCnmg0PcBoKKqPKgGu9GHQ/oygcmPrQhetwqw== +"@huggingface/inference@^4.3.2": + version "4.3.2" + resolved "https://registry.yarnpkg.com/@huggingface/inference/-/inference-4.3.2.tgz#412a9098d228c33fb3382e107ac772aba3799b23" + integrity sha512-c7MJJPDbhb0Xy3JHvO3LaRhCDnfAthdmV3UiLCYH440UkIkECGwaLHAsWg9G2gdUrmcfzybZvZ0lAQhwsiTKnA== + dependencies: + "@huggingface/jinja" "^0.5.0" + "@huggingface/tasks" "^0.19.22" + +"@huggingface/jinja@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@huggingface/jinja/-/jinja-0.5.0.tgz#0da65deb98798cd24ea42ad13f6df224ce23f443" + integrity sha512-Ptc03/jGRiYRoi0bUYKZ14MkDslsBRT24oxmsvUlfYrvQMldrxCevhPnT+hfX8awKTT8/f/0ZBBWldoeAcMHdQ== + +"@huggingface/tasks@^0.19.22": + version "0.19.22" + resolved "https://registry.yarnpkg.com/@huggingface/tasks/-/tasks-0.19.22.tgz#0759707ed1e74fd2c3d2c03a7fd20e35ea2d43af" + integrity sha512-jtRXsJZTES01X4gJ5VOUnEm3ONyyfXUcWKObbWkr/SQmjaH/kxtWqc2zVWKaxL4QLoXqXJ+T+Pi5xupMStSudQ== + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz" @@ -7389,6 +7407,11 @@ lru-memoizer@^2.2.0: lodash.clonedeep "^4.5.0" lru-cache "6.0.0" +lucide-react@^0.525.0: + version "0.525.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.525.0.tgz#5f7bcecd65e4f9b2b5b6b5d295e3376df032d5e3" + integrity sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ== + lz-string@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" From 0ca577ba0419b6ece8d6633a0a246b28669da259 Mon Sep 17 00:00:00 2001 From: pycomet Date: Sun, 6 Jul 2025 15:08:27 +0100 Subject: [PATCH 2/2] feat: add dashboard navigation and search functionality --- app/api/generate-thumbnail/route.ts | 62 +++++ app/api/test-ai/route.ts | 56 ++++ app/dashboard/page.tsx | 383 ++++++++++++++++++++++++++++ app/page.tsx | 2 +- lib/ai/fal.ts | 264 +++++++++++++++++++ lib/ai/huggingface.ts | 177 +++++++------ lib/ai/index.ts | 142 +++++++++++ lib/ai/providers.ts | 207 +++++++++++++++ lib/ai/stability.ts | 233 +++++++++++++++++ package.json | 2 + yarn.lock | 168 +++++++++++- 11 files changed, 1621 insertions(+), 75 deletions(-) create mode 100644 app/api/test-ai/route.ts create mode 100644 lib/ai/fal.ts create mode 100644 lib/ai/index.ts create mode 100644 lib/ai/providers.ts create mode 100644 lib/ai/stability.ts diff --git a/app/api/generate-thumbnail/route.ts b/app/api/generate-thumbnail/route.ts index ba4195a..df24406 100644 --- a/app/api/generate-thumbnail/route.ts +++ b/app/api/generate-thumbnail/route.ts @@ -2,7 +2,11 @@ import { NextRequest, NextResponse } from "next/server"; import { generateThumbnail, ThumbnailGenerationOptions, +<<<<<<< HEAD } from "@/lib/ai/huggingface"; +======= +} from "@/lib/ai"; +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) // Enhanced error response helper function createErrorResponse(message: string, status: number = 500, type?: string) { @@ -25,7 +29,13 @@ export async function POST(request: NextRequest) { style = "tech", model = "sdxl", quality = "balanced", +<<<<<<< HEAD userId, +======= + provider = "huggingface", + userId, + refinementPrompt, +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) } = body; // Enhanced input validation @@ -53,6 +63,7 @@ export async function POST(request: NextRequest) { ); } +<<<<<<< HEAD // Validate model and quality const validModels = ["sdxl", "flux", "realistic"]; const validQualities = ["fast", "balanced", "high"]; @@ -61,6 +72,16 @@ export async function POST(request: NextRequest) { if (!validModels.includes(model)) { return createErrorResponse( "Invalid AI model selected. Please choose a valid model.", +======= + // Validate parameters + const validProviders = ["huggingface", "stability", "fal"]; + const validQualities = ["fast", "balanced", "high"]; + const validStyles = ["tech", "gaming", "tutorial", "lifestyle"]; + + if (!validProviders.includes(provider)) { + return createErrorResponse( + "Invalid AI provider selected. Please choose a valid provider.", +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) 400, "validation" ); @@ -82,10 +103,35 @@ export async function POST(request: NextRequest) { ); } +<<<<<<< HEAD // Check for API key if (!process.env.HUGGINGFACE_API_KEY) { return createErrorResponse( "AI service is not configured. Please contact support.", +======= + // Check for API key based on provider + let apiKeyMissing = false; + let apiKeyName = ""; + + switch (provider) { + case 'huggingface': + apiKeyMissing = !process.env.HUGGINGFACE_API_KEY; + apiKeyName = "HUGGINGFACE_API_KEY"; + break; + case 'stability': + apiKeyMissing = !process.env.STABILITY_API_KEY; + apiKeyName = "STABILITY_API_KEY"; + break; + case 'fal': + apiKeyMissing = !process.env.FAL_KEY; + apiKeyName = "FAL_KEY"; + break; + } + + if (apiKeyMissing) { + return createErrorResponse( + `AI service is not configured (${apiKeyName} missing). Please contact support.`, +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) 500, "api" ); @@ -97,7 +143,13 @@ export async function POST(request: NextRequest) { style, model, quality, +<<<<<<< HEAD userId, +======= + provider, + userId, + refinementPrompt: refinementPrompt?.trim(), +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) }; console.log("Generating thumbnail with options:", { @@ -105,7 +157,13 @@ export async function POST(request: NextRequest) { style, model, quality, +<<<<<<< HEAD + userId: userId ? "***" : "none", +======= + provider, userId: userId ? "***" : "none", + refinement: refinementPrompt ? "yes" : "no", +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) }); const result = await generateThumbnail(options); @@ -121,6 +179,10 @@ export async function POST(request: NextRequest) { prompt: result.prompt, style: result.style, model: result.model, +<<<<<<< HEAD +======= + provider: result.provider, +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) parameters: result.parameters, timestamp: new Date().toISOString(), }); diff --git a/app/api/test-ai/route.ts b/app/api/test-ai/route.ts new file mode 100644 index 0000000..5f299f4 --- /dev/null +++ b/app/api/test-ai/route.ts @@ -0,0 +1,56 @@ +import { NextRequest, NextResponse } from "next/server"; +import { testConnection, testThumbnailGeneration } from "@/lib/ai/huggingface"; + +export async function POST(request: NextRequest) { + try { + console.log("๐Ÿ” Starting AI service test..."); + + // Check if API key is available + if (!process.env.HUGGINGFACE_API_KEY) { + return NextResponse.json({ + success: false, + error: "HUGGINGFACE_API_KEY environment variable is not set", + type: "configuration", + }, { status: 500 }); + } + + // Test basic connection + console.log("Testing basic connection..."); + const connectionTest = await testConnection(); + + if (!connectionTest) { + return NextResponse.json({ + success: false, + error: "HuggingFace API connection failed", + type: "connection", + }, { status: 503 }); + } + + // Test thumbnail generation + console.log("Testing thumbnail generation..."); + await testThumbnailGeneration(); + + return NextResponse.json({ + success: true, + message: "AI service is working correctly", + timestamp: new Date().toISOString(), + }); + + } catch (error) { + console.error("โŒ AI service test failed:", error); + + return NextResponse.json({ + success: false, + error: error instanceof Error ? error.message : "Unknown error", + type: "test_failed", + timestamp: new Date().toISOString(), + }, { status: 500 }); + } +} + +export async function GET() { + return NextResponse.json({ + message: "AI Test API", + usage: "Send a POST request to test the AI service", + }); +} \ No newline at end of file diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 5abee26..555742b 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -21,7 +21,12 @@ import { useRouter, useSearchParams } from "next/navigation"; import { AnimatedDiv } from "@/components/motion"; import { SearchIcon, RightArrowIcon } from "@/components/icons"; import { motion } from "framer-motion"; +<<<<<<< HEAD import { Cpu, Clock, AlertCircle, RefreshCw } from "lucide-react"; +======= +import { Cpu, Clock, AlertCircle, RefreshCw, Globe } from "lucide-react"; +import { getAvailableProviders, getModelsForProvider, type AIProvider, type AIModel } from "@/lib/ai"; +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) type ThumbnailStyle = "tech" | "gaming" | "tutorial" | "lifestyle"; @@ -30,6 +35,10 @@ interface GenerationResult { prompt: string; style: string; model: string; +<<<<<<< HEAD +======= + provider: string; +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) parameters: { steps: number; guidance_scale: number; @@ -170,16 +179,28 @@ const modelOptions = [ recommended: true, }, { +<<<<<<< HEAD value: "flux", label: "FLUX Schnell", description: "Latest model with superior quality", +======= + value: "sd15", + label: "Stable Diffusion 1.5", + description: "Fast and reliable generation", +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) icon: "๐Ÿš€", recommended: false, }, { +<<<<<<< HEAD value: "realistic", label: "Realistic SD", description: "More photorealistic outputs", +======= + value: "sd21", + label: "Stable Diffusion 2.1", + description: "Good quality output", +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) icon: "๐Ÿ“ธ", recommended: false, }, @@ -211,6 +232,7 @@ const qualityOptions = [ // Intelligent suggestion system - creates short, punchy suggestions const generateSmartSuggestions = (input: string): string[] => { +<<<<<<< HEAD const trimmedInput = input.trim().toLowerCase(); if (trimmedInput.length < 3) return []; @@ -423,6 +445,38 @@ const generateSmartSuggestions = (input: string): string[] => { .map((s) => s.replace(/\s+/g, " ").trim()) .filter((s, index, arr) => arr.indexOf(s) === index) .slice(0, 3); +======= + const inputLower = input.toLowerCase(); + const suggestions: string[] = []; + + // Content type suggestions + if (inputLower.includes("review") || inputLower.includes("unboxing")) { + suggestions.push("Tech product review with shocked reaction"); + suggestions.push("Unboxing video with surprised face"); + } + if (inputLower.includes("tutorial") || inputLower.includes("how to")) { + suggestions.push("Step-by-step tutorial with clear instructions"); + suggestions.push("Easy tutorial for beginners"); + } + if (inputLower.includes("gaming") || inputLower.includes("game")) { + suggestions.push("Epic gaming moment with intense action"); + suggestions.push("Gaming highlight reel compilation"); + } + if (inputLower.includes("lifestyle") || inputLower.includes("vlog")) { + suggestions.push("Daily lifestyle vlog with authentic moments"); + suggestions.push("Personal story with emotional journey"); + } + + // Generic suggestions if no specific content detected + if (suggestions.length === 0) { + suggestions.push("Exciting content with dramatic reveal"); + suggestions.push("Before and after transformation"); + suggestions.push("Top 10 list with numbered items"); + suggestions.push("Challenge video with surprising outcome"); + } + + return suggestions.slice(0, 3); +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) }; function DashboardContent() { @@ -433,6 +487,10 @@ function DashboardContent() { const [prompt, setPrompt] = useState(""); const [style, setStyle] = useState("tech"); +<<<<<<< HEAD +======= + const [provider, setProvider] = useState("huggingface"); +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) const [model, setModel] = useState("sdxl"); const [quality, setQuality] = useState("balanced"); const [loading, setLoading] = useState(false); @@ -441,6 +499,39 @@ function DashboardContent() { const [error, setError] = useState(null); const [retryCount, setRetryCount] = useState(0); const [suggestions, setSuggestions] = useState([]); +<<<<<<< HEAD +======= + const [refinementPrompt, setRefinementPrompt] = useState(""); + const [hasRefined, setHasRefined] = useState(false); + const [isRefining, setIsRefining] = useState(false); + const [availableProviders, setAvailableProviders] = useState([]); + const [currentModels, setCurrentModels] = useState([]); + + // Load available providers on component mount + useEffect(() => { + const providers = getAvailableProviders(); + setAvailableProviders(providers); + + // Set default provider based on availability + if (providers.length > 0) { + setProvider(providers[0].id); + } + }, []); + + // Update models when provider changes + useEffect(() => { + if (provider) { + const models = getModelsForProvider(provider); + setCurrentModels(models); + + // Set default model for the provider + if (models.length > 0) { + const recommendedModel = models.find(m => m.recommended) || models[0]; + setModel(recommendedModel.id); + } + } + }, [provider]); +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) // Handle search parameters useEffect(() => { @@ -462,7 +553,11 @@ function DashboardContent() { // Simulate progress during generation useEffect(() => { +<<<<<<< HEAD if (loading) { +======= + if (loading || isRefining) { +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) setProgress(0); const interval = setInterval(() => { setProgress((prev) => { @@ -472,7 +567,11 @@ function DashboardContent() { }, 500); return () => clearInterval(interval); } +<<<<<<< HEAD }, [loading]); +======= + }, [loading, isRefining]); +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) // Show toast notifications for errors useEffect(() => { @@ -503,6 +602,11 @@ function DashboardContent() { setError(null); setResult(null); setProgress(0); +<<<<<<< HEAD +======= + setHasRefined(false); + setRefinementPrompt(""); +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) // Simulate progress updates const progressInterval = setInterval(() => { @@ -521,6 +625,10 @@ function DashboardContent() { style, model, quality, +<<<<<<< HEAD +======= + provider, +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) userId, }), }); @@ -561,6 +669,77 @@ function DashboardContent() { }, 1000); }; +<<<<<<< HEAD +======= + const handleRefine = async () => { + if (!refinementPrompt.trim()) { + const validationError = { + type: "validation" as const, + message: "Please enter a refinement prompt", + retryable: false + }; + setError(validationError); + return; + } + + setIsRefining(true); + setError(null); + setProgress(0); + + const progressInterval = setInterval(() => { + setProgress((prev) => Math.min(prev + 10, 90)); + }, 800); + + try { + const userId = authDisabled ? "demo-user" : user?.uid; + const refinedPrompt = `${prompt} ${refinementPrompt}`.trim(); + + const response = await fetch("/api/generate-thumbnail", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + prompt: refinedPrompt, + style, + model, + quality, + provider, + userId, + refinementPrompt, + }), + }); + + clearInterval(progressInterval); + setProgress(100); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + if (data.success) { + setResult(data); + setHasRefined(true); + setRefinementPrompt(""); + message("Thumbnail refined successfully!", "success"); + } else { + const errorInfo = categorizeError(data.error); + setError(errorInfo); + } + } catch (err) { + clearInterval(progressInterval); + const errorInfo = categorizeError(err); + setError(errorInfo); + console.error("Refinement error:", err); + } finally { + setIsRefining(false); + setTimeout(() => setProgress(0), 2000); + } + }; + +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) const handleSuggestionClick = (suggestion: string) => { setPrompt(suggestion); setSuggestions([]); @@ -582,6 +761,7 @@ function DashboardContent() {
{/* Header Section */} +<<<<<<< HEAD

Create Your Perfect 

YouTube Thumbnail @@ -589,6 +769,17 @@ function DashboardContent() {

Transform your video ideas into eye-catching thumbnails that boost clicks and views +======= +

+ Create Your Perfect  +

+

+ Thumbnail +

+

+ Transform your video ideas into eye-catching thumbnails that boost + clicks and engagement +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality)

@@ -601,17 +792,28 @@ function DashboardContent() {

+<<<<<<< HEAD Describe Your Video

Tell us what your video is about and we'll create the +======= + Describe Your Content + +

+ Tell us what your content is about and we'll create the +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) perfect thumbnail

>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) placeholder="e.g., iPhone 15 Pro Max review with surprised reaction" value={prompt} onChange={(e) => setPrompt(e.target.value)} @@ -672,7 +874,13 @@ function DashboardContent() {
+<<<<<<< HEAD

Choose Your Style

+======= +

+ Choose Your Style +

+>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality)

Select the aesthetic that matches your content

@@ -711,6 +919,78 @@ function DashboardContent() { +<<<<<<< HEAD +======= + {/* AI Provider Selection */} + + + +
+

+ Choose AI Provider +

+

+ Select your preferred AI service for generating thumbnails +

+
+ +
+ {availableProviders.map((providerOption) => ( + + setProvider(providerOption.id)} + > + +
+ +
+
+

+ {providerOption.name} +

+ + {providerOption.pricing} + +
+

+ {providerOption.description} +

+

+ {providerOption.models.length} model{providerOption.models.length !== 1 ? 's' : ''} +

+
+
+
+ ))} +
+
+
+
+ +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) {/* Model and Quality Settings */} @@ -734,6 +1014,7 @@ function DashboardContent() {
+<<<<<<< HEAD {modelOptions.map((option) => ( setModel(option.value)} +======= + {currentModels.map((option) => ( + + setModel(option.id)} +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) >
+<<<<<<< HEAD {option.label} +======= + {option.name} +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) {option.recommended && ( @@ -838,9 +1139,13 @@ function DashboardContent() { isLoading={loading} fullWidth endContent={ +<<<<<<< HEAD !loading && ( ) +======= + !loading && +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) } > {loading ? "Creating Your Thumbnail..." : "Generate Thumbnail"} @@ -848,13 +1153,23 @@ function DashboardContent() { {/* Progress Bar */} +<<<<<<< HEAD {loading && ( +======= + {(loading || isRefining) && ( +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality)
+<<<<<<< HEAD

Generating...

+======= +

+ {isRefining ? "Refining..." : "Generating..."} +

+>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality)

{Math.round(progress)}%

@@ -866,7 +1181,14 @@ function DashboardContent() { size="sm" />
+<<<<<<< HEAD Using {modelOptions.find((m) => m.value === model)?.label} โ€ข {qualityOptions.find((q) => q.value === quality)?.label} quality +======= + Using{" "} + {currentModels.find((m) => m.id === model)?.name} โ€ข{" "} + {qualityOptions.find((q) => q.value === quality)?.label}{" "} + quality โ€ข {availableProviders.find((p) => p.id === provider)?.name} +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality)
@@ -950,11 +1272,18 @@ function DashboardContent() {
+<<<<<<< HEAD Parameters: {result.parameters.steps} steps, {result.parameters.guidance_scale} guidance +======= + Parameters: + + {result.parameters.steps} steps,{" "} + {result.parameters.guidance_scale} guidance +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality)
@@ -972,6 +1301,60 @@ function DashboardContent() { > Download Thumbnail +<<<<<<< HEAD +======= + + {/* Refinement Section */} + {!hasRefined && ( +
+
+

+ Refine Your Thumbnail +

+

+ Make one improvement to your thumbnail +

+
+ setRefinementPrompt(e.target.value)} + maxLength={200} + description={`${refinementPrompt.length}/200 characters`} + classNames={{ + inputWrapper: [ + "bg-default-100", + "border-1", + "border-default-200", + "hover:border-default-300", + "focus:border-primary", + ], + }} + /> + +
+ )} + + {hasRefined && ( +
+
+ โœ“ + Thumbnail refined +
+
+ )} +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality)
) : (
diff --git a/app/page.tsx b/app/page.tsx index 32beaee..a33a829 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -21,7 +21,7 @@ export default function Home() { maintaining a polished online presence.