diff --git a/app/api/generate-thumbnail/route.ts b/app/api/generate-thumbnail/route.ts new file mode 100644 index 0000000..df24406 --- /dev/null +++ b/app/api/generate-thumbnail/route.ts @@ -0,0 +1,272 @@ +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) { + 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", +<<<<<<< HEAD + userId, +======= + provider = "huggingface", + userId, + refinementPrompt, +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) + } = 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" + ); + } + +<<<<<<< HEAD + // 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.", +======= + // 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" + ); + } + + 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" + ); + } + +<<<<<<< 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" + ); + } + + // Generate thumbnail with enhanced error handling + const options: ThumbnailGenerationOptions = { + prompt: prompt.trim(), + style, + model, + quality, +<<<<<<< HEAD + userId, +======= + provider, + userId, + refinementPrompt: refinementPrompt?.trim(), +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) + }; + + console.log("Generating thumbnail with options:", { + prompt: prompt.substring(0, 50) + "...", + 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); + + // 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, +<<<<<<< HEAD +======= + provider: result.provider, +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) + 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/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 new file mode 100644 index 0000000..555742b --- /dev/null +++ b/app/dashboard/page.tsx @@ -0,0 +1,1385 @@ +"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"; +<<<<<<< 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"; + +interface GenerationResult { + imageUrl: string; + prompt: string; + style: string; + model: string; +<<<<<<< HEAD +======= + provider: string; +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) + 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, + }, + { +<<<<<<< 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, + }, +]; + +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[] => { +<<<<<<< HEAD + 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); +======= + 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() { + const { user, loading: userLoading } = useUser(); + const { message } = useMessage(); + const router = useRouter(); + const searchParams = useSearchParams(); + + 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); + const [progress, setProgress] = useState(0); + const [result, setResult] = useState(null); + 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(() => { + 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(() => { +<<<<<<< HEAD + if (loading) { +======= + if (loading || isRefining) { +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) + setProgress(0); + const interval = setInterval(() => { + setProgress((prev) => { + if (prev >= 90) return prev; + return prev + Math.random() * 15; + }); + }, 500); + return () => clearInterval(interval); + } +<<<<<<< HEAD + }, [loading]); +======= + }, [loading, isRefining]); +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) + + // 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); +<<<<<<< HEAD +======= + setHasRefined(false); + setRefinementPrompt(""); +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) + + // 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, +<<<<<<< HEAD +======= + provider, +>>>>>>> cde6b69 (feat: add dashboard navigation and search functionality) + 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); + }; + +<<<<<<< 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([]); + setError(null); + }; + + if (userLoading) { + return ( + +
+ +
+
+ ); + } + + return ( + +
+ {/* Header Section */} + +<<<<<<< HEAD +

Create Your Perfect 

+

+ YouTube Thumbnail +

+

+ 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) +

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

+<<<<<<< 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)} + 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 */} + + + +
+<<<<<<< HEAD +

Choose Your Style

+======= +

+ Choose Your Style +

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

+ Select the aesthetic that matches your content +

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

+ {option.label} +

+

+ {option.description} +

+
+
+ ))} +
+
+
+
+ +<<<<<<< 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 */} + + + +
+

+ Advanced Settings +

+

+ Fine-tune your generation parameters +

+
+ +
+ {/* Model Selection */} +
+
+ + + AI Model + +
+
+<<<<<<< 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 && ( + + 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 */} +<<<<<<< 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)}% +

+
+ +
+<<<<<<< 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) +
+
+
+
+
+ )} + + {/* 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} + +
+
+<<<<<<< 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) + +
+
+ + +<<<<<<< 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) +
+ ) : ( +
+
+ ๐ŸŽจ +
+

+ Your generated thumbnail will appear here +

+
+ )} +
+
+
+
+
+
+
+ ); +} + +export default function Dashboard() { + return ( + Loading...}> + + + ); +} 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.

+ + + + + + + + ); + } + + 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/fal.ts b/lib/ai/fal.ts new file mode 100644 index 0000000..199cfb0 --- /dev/null +++ b/lib/ai/fal.ts @@ -0,0 +1,264 @@ +// fal.ai Provider Implementation +import { ThumbnailGenerationOptions, ThumbnailResult, GenerationParameters } from './providers'; + +// Note: fal.ai client should be installed: npm install @fal-ai/client +// For now, we'll use fetch directly to avoid adding dependencies + +const FAL_API_URL = 'https://fal.run/fal-ai'; + +// Style prompts optimized for fal.ai models +const stylePrompts = { + tech: { + positive: "professional tech product, modern design, clean lighting, high quality, tech review thumbnail, sharp focus", + negative: "blurry, low quality, amateur, watermark, text overlay", + basePrompt: "tech product showcase", + }, + gaming: { + positive: "gaming setup, colorful RGB lighting, exciting atmosphere, gaming thumbnail, high energy, dramatic", + negative: "boring, dull, poor lighting, low quality", + basePrompt: "gaming content thumbnail", + }, + tutorial: { + positive: "educational content, clear presentation, professional layout, tutorial thumbnail, instructional, clean", + negative: "confusing, cluttered, messy, poor quality", + basePrompt: "tutorial content thumbnail", + }, + lifestyle: { + positive: "lifestyle photo, natural lighting, authentic, warm, lifestyle thumbnail, personal content, candid", + negative: "artificial, fake, poor lighting, low quality", + basePrompt: "lifestyle content thumbnail", + }, +}; + +const universalNegativePrompt = "low quality, blurry, amateur, watermark, text overlay, logos, copyright, distorted"; + +// Model configurations for fal.ai +const models = { + 'flux-schnell': { + id: 'flux/schnell', + name: 'FLUX.1 Schnell', + steps: { fast: 1, balanced: 2, high: 4 }, + guidance: { fast: 3.0, balanced: 5.0, high: 7.0 }, + }, + 'flux-dev': { + id: 'flux/dev', + name: 'FLUX.1 Dev', + steps: { fast: 20, balanced: 30, high: 50 }, + guidance: { fast: 5.0, balanced: 7.0, high: 8.0 }, + }, + 'hidream-fast': { + id: 'hidream-i1-fast', + name: 'HiDream I1 Fast', + steps: { fast: 8, balanced: 16, high: 25 }, + guidance: { fast: 3.0, balanced: 5.0, high: 7.0 }, + }, +}; + +export async function generateThumbnail( + options: ThumbnailGenerationOptions +): Promise { + const { + prompt, + style = "tech", + model = "flux-schnell", + quality = "balanced", + refinementPrompt, + } = options; + + // Validate API key + if (!process.env.FAL_KEY) { + throw new Error("FAL_KEY environment variable is not set"); + } + + // Get model configuration + const selectedModel = models[model as keyof typeof models]; + if (!selectedModel) { + throw new Error(`Model ${model} not found`); + } + + const styleConfig = stylePrompts[style]; + + // Build the prompt with optional refinement + let finalPrompt = `${styleConfig.basePrompt}, ${prompt}, ${styleConfig.positive}`; + if (refinementPrompt) { + finalPrompt += `, ${refinementPrompt}`; + } + + // Build negative prompt + const negativePrompt = `${styleConfig.negative}, ${universalNegativePrompt}`; + + // Generation parameters + const qualitySettings = selectedModel.steps[quality]; + const guidanceSettings = selectedModel.guidance[quality]; + + const parameters: GenerationParameters = { + model: selectedModel.id, + width: 1024, + height: 576, // 16:9 aspect ratio + steps: qualitySettings, + guidance_scale: guidanceSettings, + negative_prompt: negativePrompt, + }; + + try { + console.log("๐ŸŽจ Generating with fal.ai:", { + model: selectedModel.name, + prompt: finalPrompt.substring(0, 80) + "...", + quality, + style, + steps: parameters.steps, + guidance: parameters.guidance_scale, + dimensions: `${parameters.width}x${parameters.height}`, + }); + + // Prepare request body based on model + let requestBody: any = { + prompt: finalPrompt, + negative_prompt: negativePrompt, + image_size: { + width: parameters.width, + height: parameters.height, + }, + num_images: 1, + enable_safety_checker: true, + output_format: "jpeg", + }; + + // Add model-specific parameters + if (model === 'flux-schnell') { + requestBody.num_inference_steps = parameters.steps; + } else if (model === 'flux-dev') { + requestBody.num_inference_steps = parameters.steps; + requestBody.guidance_scale = parameters.guidance_scale; + } else if (model === 'hidream-fast') { + requestBody.num_inference_steps = parameters.steps; + requestBody.guidance_scale = parameters.guidance_scale; + } + + const response = await fetch(`${FAL_API_URL}/${selectedModel.id}`, { + method: 'POST', + headers: { + 'Authorization': `Key ${process.env.FAL_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error("fal.ai API error:", errorText); + + if (response.status === 401) { + throw new Error("Invalid fal.ai API key"); + } + if (response.status === 429) { + throw new Error("fal.ai rate limit exceeded. Please try again later."); + } + if (response.status === 400) { + throw new Error("Invalid request to fal.ai. Please check your prompt."); + } + if (response.status === 402) { + throw new Error("fal.ai payment required. Please add credits to your account."); + } + + throw new Error(`fal.ai API error: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + + if (!data.images || data.images.length === 0) { + throw new Error("No image generated by fal.ai"); + } + + // Download the image from the URL + const imageUrl = data.images[0].url; + const imageResponse = await fetch(imageUrl); + + if (!imageResponse.ok) { + throw new Error("Failed to download generated image"); + } + + const imageBlob = await imageResponse.blob(); + + console.log("โœ… fal.ai generation successful"); + + return { + imageBlob, + prompt: finalPrompt, + style, + model: selectedModel.name, + provider: 'fal', + parameters, + }; + } catch (error) { + console.error("โŒ fal.ai error:", error); + + if (error instanceof Error) { + throw error; + } + + throw new Error(`Failed to generate thumbnail with fal.ai: ${error}`); + } +} + +export async function testConnection(): Promise { + try { + if (!process.env.FAL_KEY) { + return false; + } + + console.log("๐Ÿ” Testing fal.ai connection..."); + + // Test with a simple request to flux-schnell (fastest model) + const response = await fetch(`${FAL_API_URL}/flux/schnell`, { + method: 'POST', + headers: { + 'Authorization': `Key ${process.env.FAL_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + prompt: "test image", + image_size: { + width: 512, + height: 512, + }, + num_images: 1, + num_inference_steps: 1, + }), + }); + + if (response.ok) { + console.log("โœ… fal.ai connection test successful"); + return true; + } else if (response.status === 402) { + console.log("โš ๏ธ fal.ai connection OK but requires payment"); + return true; // Connection works, just needs credits + } else { + console.error("โŒ fal.ai connection test failed:", response.status); + return false; + } + } catch (error) { + console.error("โŒ fal.ai connection test failed:", error); + return false; + } +} + +export async function testThumbnailGeneration(): Promise { + console.log("๐Ÿงช Testing fal.ai thumbnail generation..."); + + try { + const result = await generateThumbnail({ + prompt: "iPhone review", + style: "tech", + model: "flux-schnell", + quality: "fast", + }); + + console.log("โœ… fal.ai test generation successful"); + console.log("Prompt used:", result.prompt); + console.log("Image blob size:", result.imageBlob.size, "bytes"); + } catch (error) { + console.error("โŒ fal.ai test generation failed:", error); + throw error; + } +} \ No newline at end of file diff --git a/lib/ai/huggingface.ts b/lib/ai/huggingface.ts new file mode 100644 index 0000000..9c15f43 --- /dev/null +++ b/lib/ai/huggingface.ts @@ -0,0 +1,249 @@ +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" | "sd15" | "sd21"; + quality?: "fast" | "balanced" | "high"; +} + +export interface ThumbnailResult { + imageBlob: Blob; + prompt: string; + style: string; + model: string; + provider: string; + parameters: GenerationParameters; +} + +interface GenerationParameters { + model: string; + steps: number; + guidance_scale: number; + negative_prompt: string; + width: number; + height: number; +} + +// FREE TIER WORKING MODELS - No gated access required +const models = { + sdxl: { + id: "stabilityai/stable-diffusion-xl-base-1.0", + name: "Stable Diffusion XL", + description: "Best balance of quality and speed", + recommended: true, + }, + sd15: { + id: "runwayml/stable-diffusion-v1-5", + name: "Stable Diffusion 1.5", + description: "Fast and reliable generation", + recommended: false, + }, + sd21: { + id: "stabilityai/stable-diffusion-2-1", + name: "Stable Diffusion 2.1", + description: "Good quality output", + recommended: false, + }, +}; + +// Optimized quality presets for free tier +const qualityPresets = { + fast: { + steps: 15, + guidance_scale: 7.0, + description: "Quick generation (~10s)", + }, + balanced: { + steps: 25, + guidance_scale: 7.5, + description: "Good quality (~20s)", + }, + high: { + steps: 35, + guidance_scale: 8.0, + description: "Best quality (~30s)", + }, +}; + +// Simple, effective prompts that work well with free models +const stylePrompts = { + tech: { + positive: "professional tech product, modern design, clean lighting, high quality", + negative: "blurry, low quality, amateur", + basePrompt: "tech thumbnail", + }, + gaming: { + positive: "gaming setup, colorful lights, exciting atmosphere, high quality", + negative: "boring, dull, poor lighting", + basePrompt: "gaming thumbnail", + }, + tutorial: { + positive: "educational content, clear presentation, professional layout", + negative: "confusing, cluttered, messy", + basePrompt: "tutorial thumbnail", + }, + lifestyle: { + positive: "lifestyle photo, natural lighting, authentic, warm", + negative: "artificial, fake, poor lighting", + basePrompt: "lifestyle thumbnail", + }, +}; + +// Minimal negative prompt for better results +const universalNegativePrompt = "low quality, blurry, amateur, watermark"; + +export async function generateThumbnail( + options: ThumbnailGenerationOptions +): Promise { + const { + prompt, + style = "tech", + model = "sdxl", + quality = "balanced", + } = options; + + // Validate API key + if (!process.env.HUGGINGFACE_API_KEY) { + throw new Error("HUGGINGFACE_API_KEY environment variable is not set"); + } + + // Get model and quality settings + const selectedModel = models[model]; + const qualitySettings = qualityPresets[quality]; + const styleConfig = stylePrompts[style]; + + // Build simple, effective prompt + const optimizedPrompt = `${styleConfig.basePrompt}, ${prompt}, ${styleConfig.positive}`; + + // Simple negative prompt + const negativePrompt = `${styleConfig.negative}, ${universalNegativePrompt}`; + + // Generation parameters optimized for free tier + const parameters: GenerationParameters = { + model: selectedModel.id, + width: 768, // Smaller size for free tier + height: 432, // 16:9 ratio + steps: qualitySettings.steps, + guidance_scale: qualitySettings.guidance_scale, + negative_prompt: negativePrompt, + }; + + try { + console.log("๐ŸŽจ Generating with FREE TIER optimized parameters:", { + model: selectedModel.name, + prompt: optimizedPrompt.substring(0, 80) + "...", + quality, + style, + steps: parameters.steps, + guidance: parameters.guidance_scale, + dimensions: `${parameters.width}x${parameters.height}`, + }); + + 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, + }, + }); + + console.log("โœ… Thumbnail generation successful"); + + return { + imageBlob: response as unknown as Blob, + prompt: optimizedPrompt, + style, + model: selectedModel.name, + provider: 'huggingface', + parameters, + }; + } catch (error) { + console.error("โŒ HuggingFace API error:", error); + + // Enhanced error handling for common free tier issues + if (error instanceof Error) { + if (error.message.includes("unauthorized") || error.message.includes("401")) { + throw new Error("Invalid HuggingFace API key. Please check your API key."); + } + if (error.message.includes("rate") || error.message.includes("limit") || error.message.includes("quota")) { + throw new Error("HuggingFace quota exceeded. Try again in a few minutes or upgrade your account."); + } + if (error.message.includes("gated") || error.message.includes("restricted") || error.message.includes("access")) { + throw new Error(`Model ${selectedModel.name} requires special access. Using alternative model.`); + } + if (error.message.includes("503") || error.message.includes("unavailable")) { + throw new Error("HuggingFace service temporarily unavailable. Try again in a few minutes."); + } + } + + 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 { + console.log("๐Ÿ” Testing HuggingFace connection with free tier model..."); + + const response = await hf.textToImage({ + model: models.sd15.id, // Use SD 1.5 for testing (most reliable) + inputs: "simple test image", + parameters: { + width: 512, + height: 512, + num_inference_steps: 10, + guidance_scale: 7.5, + }, + }); + + console.log("โœ… HuggingFace connection test successful"); + return response !== null; + } catch (error) { + console.error("โŒ HuggingFace connection test failed:", error); + return false; + } +} + +// Test function optimized for free tier +export async function testThumbnailGeneration(): Promise { + console.log("๐Ÿงช Testing thumbnail generation with free tier settings..."); + + try { + const result = await generateThumbnail({ + prompt: "iPhone review", + style: "tech", + model: "sd15", // Use most reliable model + quality: "fast", // Use fastest setting + }); + + console.log("โœ… Test thumbnail generation successful"); + console.log("Prompt used:", result.prompt); + console.log("Image blob size:", result.imageBlob.size, "bytes"); + } catch (error) { + console.error("โŒ Test thumbnail generation failed:", error); + throw error; + } +} diff --git a/lib/ai/index.ts b/lib/ai/index.ts new file mode 100644 index 0000000..06455f6 --- /dev/null +++ b/lib/ai/index.ts @@ -0,0 +1,142 @@ +// Central AI Provider Manager +import { + getProvider, + getAvailableProviders, + getModelsForProvider, + providerSupportsRefinement, + ThumbnailGenerationOptions, + ThumbnailResult, + AIProvider, + AIModel +} from './providers'; + +export { + getAvailableProviders, + getModelsForProvider, + providerSupportsRefinement, + type AIProvider, + type AIModel, + type ThumbnailGenerationOptions, + type ThumbnailResult +}; + +/** + * Generate a thumbnail using the specified AI provider + */ +export async function generateThumbnail( + options: ThumbnailGenerationOptions +): Promise { + const { provider = 'huggingface' } = options; + + const selectedProvider = getProvider(provider); + if (!selectedProvider) { + throw new Error(`Provider ${provider} not found`); + } + + console.log(`๐ŸŽฏ Using provider: ${selectedProvider.name}`); + + try { + return await selectedProvider.generateThumbnail(options); + } catch (error) { + console.error(`โŒ Error with provider ${selectedProvider.name}:`, error); + throw error; + } +} + +/** + * Test connection to a specific AI provider + */ +export async function testConnection(provider: string): Promise { + const selectedProvider = getProvider(provider); + if (!selectedProvider) { + throw new Error(`Provider ${provider} not found`); + } + + console.log(`๐Ÿ” Testing connection to ${selectedProvider.name}...`); + + try { + return await selectedProvider.testConnection(); + } catch (error) { + console.error(`โŒ Connection test failed for ${selectedProvider.name}:`, error); + return false; + } +} + +/** + * Test all available providers and return their status + */ +export async function testAllProviders(): Promise> { + const providers = getAvailableProviders(); + const results: Record = {}; + + console.log('๐Ÿงช Testing all AI providers...'); + + for (const provider of providers) { + try { + results[provider.id] = await testConnection(provider.id); + } catch (error) { + console.error(`โŒ Test failed for ${provider.name}:`, error); + results[provider.id] = false; + } + } + + return results; +} + +/** + * Get the best available provider based on API key availability + */ +export async function getBestAvailableProvider(): Promise { + const providers = getAvailableProviders(); + + // Test providers in order of preference + const preferenceOrder = ['stability', 'fal', 'huggingface']; + + for (const providerId of preferenceOrder) { + const provider = providers.find(p => p.id === providerId); + if (provider) { + const isAvailable = await testConnection(providerId); + if (isAvailable) { + console.log(`โœ… Best available provider: ${provider.name}`); + return providerId; + } + } + } + + // Fallback to first provider + console.log('โš ๏ธ No providers available, falling back to HuggingFace'); + return 'huggingface'; +} + +/** + * Get provider availability status + */ +export async function getProviderStatus(): Promise> { + const providers = getAvailableProviders(); + const status: Record = {}; + + for (const provider of providers) { + const isAvailable = await testConnection(provider.id); + status[provider.id] = { + available: isAvailable, + name: provider.name, + description: provider.description, + pricing: provider.pricing, + models: provider.models, + }; + } + + return status; +} \ No newline at end of file diff --git a/lib/ai/providers.ts b/lib/ai/providers.ts new file mode 100644 index 0000000..5d2d11d --- /dev/null +++ b/lib/ai/providers.ts @@ -0,0 +1,207 @@ +// Provider abstraction for AI image generation services +export interface AIProvider { + id: string; + name: string; + description: string; + pricing: string; + models: AIModel[]; + supportsRefinement: boolean; + generateThumbnail: (options: ThumbnailGenerationOptions) => Promise; + testConnection: () => Promise; +} + +export interface AIModel { + id: string; + name: string; + description: string; + icon: string; + recommended: boolean; + speed: 'fast' | 'medium' | 'slow'; + quality: 'good' | 'high' | 'excellent'; +} + +export interface ThumbnailGenerationOptions { + prompt: string; + style?: "tech" | "gaming" | "tutorial" | "lifestyle"; + userId?: string; + model?: string; + quality?: "fast" | "balanced" | "high"; + provider?: string; + refinementPrompt?: string; +} + +export interface ThumbnailResult { + imageBlob: Blob; + prompt: string; + style: string; + model: string; + provider: string; + parameters: GenerationParameters; +} + +export interface GenerationParameters { + model: string; + steps: number; + guidance_scale: number; + negative_prompt: string; + width: number; + height: number; +} + +// Provider configurations +export const AI_PROVIDERS: Record = { + stability: { + id: 'stability', + name: 'Stability AI', + description: 'Free for personal & commercial use under $1M revenue', + pricing: 'FREE', + supportsRefinement: true, + models: [ + { + id: 'sd-3.5-large', + name: 'Stable Diffusion 3.5 Large', + description: '8B parameters, superior quality', + icon: '๐ŸŽฏ', + recommended: true, + speed: 'medium', + quality: 'excellent' + }, + { + id: 'sd-3.5-turbo', + name: 'Stable Diffusion 3.5 Turbo', + description: '4-step generation, ultra-fast', + icon: 'โšก', + recommended: false, + speed: 'fast', + quality: 'high' + }, + { + id: 'sdxl', + name: 'Stable Diffusion XL', + description: 'Proven quality, reliable', + icon: '๐Ÿ–ผ๏ธ', + recommended: false, + speed: 'medium', + quality: 'high' + } + ], + generateThumbnail: async (options: ThumbnailGenerationOptions) => { + const { generateThumbnail: stabilityGenerate } = await import('./stability'); + return stabilityGenerate(options); + }, + testConnection: async () => { + const { testConnection: stabilityTest } = await import('./stability'); + return stabilityTest(); + } + }, + fal: { + id: 'fal', + name: 'fal.ai', + description: 'Pay-per-use, excellent value', + pricing: '~$0.003/image', + supportsRefinement: true, + models: [ + { + id: 'flux-schnell', + name: 'FLUX.1 Schnell', + description: 'Ultra-fast, 333 images per $1', + icon: '๐Ÿš€', + recommended: true, + speed: 'fast', + quality: 'high' + }, + { + id: 'flux-dev', + name: 'FLUX.1 Dev', + description: 'Premium quality, 40 images per $1', + icon: '๐Ÿ’Ž', + recommended: false, + speed: 'medium', + quality: 'excellent' + }, + { + id: 'hidream-fast', + name: 'HiDream I1 Fast', + description: 'New model, 16 steps', + icon: 'โœจ', + recommended: false, + speed: 'fast', + quality: 'high' + } + ], + generateThumbnail: async (options: ThumbnailGenerationOptions) => { + const { generateThumbnail: falGenerate } = await import('./fal'); + return falGenerate(options); + }, + testConnection: async () => { + const { testConnection: falTest } = await import('./fal'); + return falTest(); + } + }, + huggingface: { + id: 'huggingface', + name: 'HuggingFace', + description: 'Free tier with quota limits', + pricing: 'FREE (Limited)', + supportsRefinement: true, + models: [ + { + id: 'sdxl', + name: 'Stable Diffusion XL', + description: 'Best balance of quality and speed', + icon: 'โšก', + recommended: true, + speed: 'medium', + quality: 'high' + }, + { + id: 'sd15', + name: 'Stable Diffusion 1.5', + description: 'Fast and reliable generation', + icon: '๐Ÿš€', + recommended: false, + speed: 'fast', + quality: 'good' + }, + { + id: 'sd21', + name: 'Stable Diffusion 2.1', + description: 'Good quality output', + icon: '๐Ÿ“ธ', + recommended: false, + speed: 'medium', + quality: 'good' + } + ], + generateThumbnail: async (options: ThumbnailGenerationOptions) => { + const { generateThumbnail: hfGenerate } = await import('./huggingface'); + return hfGenerate(options); + }, + testConnection: async () => { + const { testConnection: hfTest } = await import('./huggingface'); + return hfTest(); + } + } +}; + +// Get available providers +export function getAvailableProviders(): AIProvider[] { + return Object.values(AI_PROVIDERS); +} + +// Get provider by ID +export function getProvider(providerId: string): AIProvider | null { + return AI_PROVIDERS[providerId] || null; +} + +// Get models for a specific provider +export function getModelsForProvider(providerId: string): AIModel[] { + const provider = getProvider(providerId); + return provider?.models || []; +} + +// Check if provider supports refinement +export function providerSupportsRefinement(providerId: string): boolean { + const provider = getProvider(providerId); + return provider?.supportsRefinement || false; +} \ No newline at end of file diff --git a/lib/ai/stability.ts b/lib/ai/stability.ts new file mode 100644 index 0000000..d80dbd9 --- /dev/null +++ b/lib/ai/stability.ts @@ -0,0 +1,233 @@ +// Stability AI Provider Implementation +import { ThumbnailGenerationOptions, ThumbnailResult, GenerationParameters } from './providers'; + +const STABILITY_API_URL = 'https://api.stability.ai/v1/generation'; + +// Style prompts optimized for Stability AI +const stylePrompts = { + tech: { + positive: "professional tech product, modern design, clean lighting, high quality, tech review thumbnail", + negative: "blurry, low quality, amateur, watermark, text overlay", + basePrompt: "tech product showcase", + }, + gaming: { + positive: "gaming setup, colorful RGB lighting, exciting atmosphere, gaming thumbnail, high energy", + negative: "boring, dull, poor lighting, low quality", + basePrompt: "gaming content thumbnail", + }, + tutorial: { + positive: "educational content, clear presentation, professional layout, tutorial thumbnail, instructional", + negative: "confusing, cluttered, messy, poor quality", + basePrompt: "tutorial content thumbnail", + }, + lifestyle: { + positive: "lifestyle photo, natural lighting, authentic, warm, lifestyle thumbnail, personal content", + negative: "artificial, fake, poor lighting, low quality", + basePrompt: "lifestyle content thumbnail", + }, +}; + +const universalNegativePrompt = "low quality, blurry, amateur, watermark, text overlay, logos, copyright"; + +// Model configurations for Stability AI +const models = { + 'sd-3.5-large': { + id: 'stable-diffusion-3-5-large', + name: 'Stable Diffusion 3.5 Large', + steps: { fast: 20, balanced: 30, high: 50 }, + guidance: { fast: 5.0, balanced: 7.0, high: 8.0 }, + }, + 'sd-3.5-turbo': { + id: 'stable-diffusion-3-5-turbo', + name: 'Stable Diffusion 3.5 Turbo', + steps: { fast: 4, balanced: 6, high: 8 }, + guidance: { fast: 3.0, balanced: 5.0, high: 7.0 }, + }, + 'sdxl': { + id: 'stable-diffusion-xl-1024-v1-0', + name: 'Stable Diffusion XL', + steps: { fast: 15, balanced: 25, high: 35 }, + guidance: { fast: 6.0, balanced: 7.5, high: 8.5 }, + }, +}; + +export async function generateThumbnail( + options: ThumbnailGenerationOptions +): Promise { + const { + prompt, + style = "tech", + model = "sd-3.5-large", + quality = "balanced", + refinementPrompt, + } = options; + + // Validate API key + if (!process.env.STABILITY_API_KEY) { + throw new Error("STABILITY_API_KEY environment variable is not set"); + } + + // Get model configuration + const selectedModel = models[model as keyof typeof models]; + if (!selectedModel) { + throw new Error(`Model ${model} not found`); + } + + const styleConfig = stylePrompts[style]; + + // Build the prompt with optional refinement + let finalPrompt = `${styleConfig.basePrompt}, ${prompt}, ${styleConfig.positive}`; + if (refinementPrompt) { + finalPrompt += `, ${refinementPrompt}`; + } + + // Build negative prompt + const negativePrompt = `${styleConfig.negative}, ${universalNegativePrompt}`; + + // Generation parameters + const qualitySettings = selectedModel.steps[quality]; + const guidanceSettings = selectedModel.guidance[quality]; + + const parameters: GenerationParameters = { + model: selectedModel.id, + width: 1024, + height: 576, // 16:9 aspect ratio + steps: qualitySettings, + guidance_scale: guidanceSettings, + negative_prompt: negativePrompt, + }; + + try { + console.log("๐ŸŽจ Generating with Stability AI:", { + model: selectedModel.name, + prompt: finalPrompt.substring(0, 80) + "...", + quality, + style, + steps: parameters.steps, + guidance: parameters.guidance_scale, + dimensions: `${parameters.width}x${parameters.height}`, + }); + + const response = await fetch(`${STABILITY_API_URL}/${selectedModel.id}/text-to-image`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${process.env.STABILITY_API_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + text_prompts: [ + { + text: finalPrompt, + weight: 1.0, + }, + { + text: negativePrompt, + weight: -1.0, + }, + ], + cfg_scale: parameters.guidance_scale, + height: parameters.height, + width: parameters.width, + samples: 1, + steps: parameters.steps, + style_preset: style === 'tech' ? 'enhance' : + style === 'gaming' ? 'neon-punk' : + style === 'tutorial' ? 'digital-art' : 'photographic', + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error("Stability API error:", errorText); + + if (response.status === 401) { + throw new Error("Invalid Stability AI API key"); + } + if (response.status === 429) { + throw new Error("Stability AI rate limit exceeded. Please try again later."); + } + if (response.status === 400) { + throw new Error("Invalid request to Stability AI. Please check your prompt."); + } + + throw new Error(`Stability AI API error: ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + + if (!data.artifacts || data.artifacts.length === 0) { + throw new Error("No image generated by Stability AI"); + } + + // Convert base64 to blob + const base64Image = data.artifacts[0].base64; + const imageBuffer = Buffer.from(base64Image, 'base64'); + const imageBlob = new Blob([imageBuffer], { type: 'image/png' }); + + console.log("โœ… Stability AI generation successful"); + + return { + imageBlob, + prompt: finalPrompt, + style, + model: selectedModel.name, + provider: 'stability', + parameters, + }; + } catch (error) { + console.error("โŒ Stability AI error:", error); + + if (error instanceof Error) { + throw error; + } + + throw new Error(`Failed to generate thumbnail with Stability AI: ${error}`); + } +} + +export async function testConnection(): Promise { + try { + if (!process.env.STABILITY_API_KEY) { + return false; + } + + console.log("๐Ÿ” Testing Stability AI connection..."); + + const response = await fetch('https://api.stability.ai/v1/user/account', { + headers: { + 'Authorization': `Bearer ${process.env.STABILITY_API_KEY}`, + }, + }); + + if (response.ok) { + console.log("โœ… Stability AI connection test successful"); + return true; + } else { + console.error("โŒ Stability AI connection test failed:", response.status); + return false; + } + } catch (error) { + console.error("โŒ Stability AI connection test failed:", error); + return false; + } +} + +export async function testThumbnailGeneration(): Promise { + console.log("๐Ÿงช Testing Stability AI thumbnail generation..."); + + try { + const result = await generateThumbnail({ + prompt: "iPhone review", + style: "tech", + model: "sdxl", + quality: "fast", + }); + + console.log("โœ… Stability AI test generation successful"); + console.log("Prompt used:", result.prompt); + console.log("Image blob size:", result.imageBlob.size, "bytes"); + } catch (error) { + console.error("โŒ Stability AI test generation failed:", error); + throw error; + } +} \ No newline at end of file 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..fcbc829 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", @@ -33,6 +34,7 @@ "@types/react-slick": "^0.23.13", "@typescript-eslint/eslint-plugin": "^7.10.0", "@typescript-eslint/parser": "^7.10.0", + "ai": "^4.3.16", "autoprefixer": "10.4.19", "clsx": "^2.0.0", "cogo-toast": "^4.2.3", @@ -52,6 +54,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", @@ -61,6 +64,7 @@ "react-hook-form": "^7.52.0", "react-slick": "^0.30.2", "sharp": "^0.33.4", + "stability-ai": "^0.7.0", "tailwind-variants": "^0.1.20", "tailwindcss": "3.4.3", "ts-node": "^10.9.2", diff --git a/yarn.lock b/yarn.lock index 45b3f5c..d7eeffe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,6 +7,41 @@ resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.0.tgz#728c484f4e10df03d5a3acd0d8adcbbebff8ad63" integrity sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ== +"@ai-sdk/provider-utils@2.2.8": + version "2.2.8" + resolved "https://registry.yarnpkg.com/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz#ad11b92d5a1763ab34ba7b5fc42494bfe08b76d1" + integrity sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA== + dependencies: + "@ai-sdk/provider" "1.1.3" + nanoid "^3.3.8" + secure-json-parse "^2.7.0" + +"@ai-sdk/provider@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@ai-sdk/provider/-/provider-1.1.3.tgz#ebdda8077b8d2b3f290dcba32c45ad19b2704681" + integrity sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg== + dependencies: + json-schema "^0.4.0" + +"@ai-sdk/react@1.2.12": + version "1.2.12" + resolved "https://registry.yarnpkg.com/@ai-sdk/react/-/react-1.2.12.tgz#f4250b6df566b170af98a71d5708b52108dd0ce1" + integrity sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g== + dependencies: + "@ai-sdk/provider-utils" "2.2.8" + "@ai-sdk/ui-utils" "1.2.11" + swr "^2.2.5" + throttleit "2.1.0" + +"@ai-sdk/ui-utils@1.2.11": + version "1.2.11" + resolved "https://registry.yarnpkg.com/@ai-sdk/ui-utils/-/ui-utils-1.2.11.tgz#4f815589d08d8fef7292ade54ee5db5d09652603" + integrity sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w== + dependencies: + "@ai-sdk/provider" "1.1.3" + "@ai-sdk/provider-utils" "2.2.8" + zod-to-json-schema "^3.24.1" + "@alloc/quick-lru@^5.2.0": version "5.2.0" resolved "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz" @@ -879,6 +914,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" @@ -2302,6 +2355,11 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@opentelemetry/api@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" + integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" @@ -3865,6 +3923,11 @@ dependencies: "@types/node" "*" +"@types/diff-match-patch@^1.0.36": + version "1.0.36" + resolved "https://registry.yarnpkg.com/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz#dcef10a69d357fe9d43ac4ff2eca6b85dbf466af" + integrity sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg== + "@types/express-serve-static-core@^4.17.33": version "4.19.3" resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.3.tgz" @@ -4253,6 +4316,18 @@ agent-base@^7.0.2: dependencies: debug "^4.3.4" +ai@^4.3.16: + version "4.3.16" + resolved "https://registry.yarnpkg.com/ai/-/ai-4.3.16.tgz#c9446da1024cdc1dfe2913d151b70c91d40f2378" + integrity sha512-KUDwlThJ5tr2Vw0A1ZkbDKNME3wzWhuVfAOwIvFUzl1TPVDFAXDFTXio3p+jaKneB+dKNCvFFlolYmmgHttG1g== + dependencies: + "@ai-sdk/provider" "1.1.3" + "@ai-sdk/provider-utils" "2.2.8" + "@ai-sdk/react" "1.2.12" + "@ai-sdk/ui-utils" "1.2.11" + "@opentelemetry/api" "1.9.0" + jsondiffpatch "0.6.0" + ajv@^6.12.4: version "6.12.6" resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" @@ -4496,6 +4571,15 @@ axe-core@=4.7.0: resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz" integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== +axios@^1.6.8: + version "1.10.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.10.0.tgz#af320aee8632eaf2a400b6a1979fa75856f38d54" + integrity sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + axobject-query@^3.2.1: version "3.2.1" resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz" @@ -4732,6 +4816,11 @@ chalk@^4.0.0, chalk@^4.1.0: ansi-styles "^4.1.0" supports-color "^7.1.0" +chalk@^5.3.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.4.1.tgz#1b48bf0963ec158dce2aacf69c093ae2dd2092d8" + integrity sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w== + char-regex@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" @@ -5084,6 +5173,11 @@ didyoumean@^1.2.2: resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== +diff-match-patch@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.5.tgz#abb584d5f10cd1196dfc55aa03701592ae3f7b37" + integrity sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw== + diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" @@ -5852,6 +5946,11 @@ flatted@^3.2.9: resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz" @@ -5902,6 +6001,15 @@ fs-constants@^1.0.0: resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== +fs-extra@^11.2.0: + version "11.3.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.3.0.tgz#0daced136bbaf65a555a326719af931adc7a314d" + integrity sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" @@ -6130,7 +6238,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9: +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -7098,6 +7206,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" @@ -7122,6 +7235,24 @@ json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsondiffpatch@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/jsondiffpatch/-/jsondiffpatch-0.6.0.tgz#daa6a25bedf0830974c81545568d5f671c82551f" + integrity sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ== + dependencies: + "@types/diff-match-patch" "^1.0.36" + chalk "^5.3.0" + diff-match-patch "^1.0.5" + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + jsonwebtoken@^9.0.0: version "9.0.2" resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz" @@ -7389,6 +7520,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" @@ -7523,6 +7659,11 @@ nanoid@^3.3.6, nanoid@^3.3.7: resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz" integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== +nanoid@^3.3.8: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + napi-build-utils@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz" @@ -8003,6 +8144,11 @@ protobufjs@7.3.0, protobufjs@^7.2.5, protobufjs@^7.2.6: "@types/node" ">=13.7.0" long "^5.0.0" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + psl@^1.1.33: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" @@ -8336,6 +8482,11 @@ scroll-into-view-if-needed@3.0.10: dependencies: compute-scroll-into-view "^3.0.2" +secure-json-parse@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz#5a5f9cd6ae47df23dba3151edd06855d47e09862" + integrity sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw== + semver@^6.1.0, semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" @@ -8483,6 +8634,16 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +stability-ai@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/stability-ai/-/stability-ai-0.7.0.tgz#425feb9eea3d523a3b26756656c5acef9b82a3f7" + integrity sha512-uXKhaCgSF0J379zSuLgj8hBEheCV26fF2B8eTFyU3qWO2TzDUlGmLdyQmF6ZGDkADSVyxuxKAPcLeKcQZx9dPQ== + dependencies: + axios "^1.6.8" + dotenv "^16.4.5" + fs-extra "^11.2.0" + uuid "^9.0.1" + stack-utils@^2.0.3: version "2.0.6" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" @@ -8709,6 +8870,14 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +swr@^2.2.5: + version "2.3.4" + resolved "https://registry.yarnpkg.com/swr/-/swr-2.3.4.tgz#60bcb5b97cae157a6ef69eff0ed2beb9010eba69" + integrity sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg== + dependencies: + dequal "^2.0.3" + use-sync-external-store "^1.4.0" + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -8827,6 +8996,11 @@ thenify-all@^1.0.0: dependencies: any-promise "^1.0.0" +throttleit@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-2.1.0.tgz#a7e4aa0bf4845a5bd10daa39ea0c783f631a07b4" + integrity sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw== + tiny-case@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" @@ -9030,6 +9204,11 @@ universalify@^0.2.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + update-browserslist-db@^1.0.13: version "1.0.16" resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz" @@ -9093,6 +9272,11 @@ use-sidecar@^1.1.2: detect-node-es "^1.1.0" tslib "^2.0.0" +use-sync-external-store@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0" + integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A== + util-deprecate@^1.0.1, util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" @@ -9362,3 +9546,8 @@ yup@^1.4.0: tiny-case "^1.0.3" toposort "^2.0.2" type-fest "^2.19.0" + +zod-to-json-schema@^3.24.1: + version "3.24.6" + resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz#5920f020c4d2647edfbb954fa036082b92c9e12d" + integrity sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==