Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
41 KiB
JSON
1 line
41 KiB
JSON
{"repo": "300dpi-co/pixly", "files": {"app/Services/BlogAIService.php": {"99e7b33467817c60024f776bd28554aa74d4d15d": {"t": 1785746146.9516237, "n": 10838, "h": "3a34e1f53e91c405", "c": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Services;\n\n/**\n * Blog AI Service\n *\n * Handles AI-powered content generation for blog posts.\n */\nclass BlogAIService\n{\n private string $apiKey;\n private string $endpoint;\n private string $model;\n\n public function __construct()\n {\n $this->apiKey = config('api.deepseek.key', '');\n $this->endpoint = config('api.deepseek.endpoint', 'https://api.deepseek.com');\n $this->model = config('api.deepseek.model', 'deepseek-chat');\n }\n\n /**\n * Generate a complete blog post\n */\n public function generatePost(string $topic, string $tone = 'professional', string $length = 'medium', string $keywords = ''): array\n {\n $wordCounts = [\n 'short' => '500-700',\n 'medium' => '1000-1200',\n 'long' => '1800-2200',\n ];\n\n $targetWords = $wordCounts[$length] ?? '1000-1200';\n\n $prompt = $this->buildGenerationPrompt($topic, $tone, $targetWords, $keywords);\n\n $response = $this->callAPI($prompt);\n\n return $this->parseGeneratedContent($response, $topic);\n }\n\n /**\n * Improve existing content\n */\n public function improveContent(string $content, string $action = 'improve'): array\n {\n $prompts = [\n 'improve' => \"Improve the following blog content. Make it more engaging, clear, and well-structured while maintaining the original message. Fix any grammatical errors and improve the flow:\\n\\n{$content}\\n\\nProvide only the improved content, no explanations.\",\n\n 'expand' => \"Expand the following content with more details, examples, and explanations. Add relevant information to make it more comprehensive. Double the length while keeping it focused:\\n\\n{$content}\\n\\nProvide only the expanded content.\",\n\n 'summarize' => \"Summarize the following content into a concise version that retains the key points. Reduce it to about 30% of the original length:\\n\\n{$content}\\n\\nProvide only the summarized content.\",\n\n 'seo' => \"Optimize the following content for SEO. Add relevant keywords naturally, improve headings for better structure, and make it more search-engine friendly while maintaining readability:\\n\\n{$content}\\n\\nProvide only the optimized content.\",\n ];\n\n $prompt = $prompts[$action] ?? $prompts['improve'];\n\n $response = $this->callAPI($prompt);\n\n return [\n 'content' => $this->cleanContent($response),\n ];\n }\n\n /**\n * Generate SEO metadata\n */\n public function generateMetadata(string $title, string $content): array\n {\n $prompt = \"Based on this blog post, generate SEO metadata:\n\nTitle: {$title}\nContent (excerpt): \" . substr(strip_tags($content), 0, 500) . \"\n\nProvide in this exact JSON format:\n{\n \\\"meta_title\\\": \\\"SEO optimized title under 60 chars\\\",\n \\\"meta_description\\\": \\\"Compelling meta description under 160 chars\\\",\n \\\"keywords\\\": \\\"keyword1, keyword2, keyword3, keyword4, keyword5\\\"\n}\n\nReturn ONLY the JSON, no other text.\";\n\n $response = $this->callAPI($prompt);\n\n try {\n $data = json_decode($response, true);\n if ($data) {\n return $data;\n }\n } catch (\\Exception $e) {\n // Fall through to defaults\n }\n\n return [\n 'meta_title' => substr($title, 0, 60),\n 'meta_description' => substr(strip_tags($content), 0, 160),\n 'keywords' => '',\n ];\n }\n\n /**\n * Generate tags from content\n */\n public function generateTags(string $title, string $content): array\n {\n $prompt = \"Analyze this blog post and suggest 5-8 relevant tags (single words or short phrases).\n\nTitle: {$title}\nContent: \" . substr(strip_tags($content), 0, 1000) . \"\n\nReturn ONLY a comma-separated list of tags, nothing else. Example: technology, artificial intelligence, machine learning, data science\";\n\n $response = $this->callAPI($prompt);\n\n $tags = array_filter(array_map('trim', explode(',', $response)));\n return array_slice($tags, 0, 8);\n }\n\n /**\n * Build generation prompt\n */\n private function buildGenerationPrompt(string $topic, string $tone, string $wordCount, string $keywords): string\n {\n $toneInstructions = [\n 'professional' => 'Use a professional, authoritative tone. Be informative and credible.',\n 'casual' => 'Use a casual, conversational tone. Be friendly and approachable.',\n 'friendly' => 'Use a warm, friendly tone. Be encouraging and supportive.',\n 'authoritative' => 'Use an expert, authoritative tone. Demonstrate deep knowledge.',\n 'humorous' => 'Use a light, humorous tone where appropriate. Keep it engaging and fun.',\n ];\n\n $toneGuide = $toneInstructions[$tone] ?? $toneInstructions['professional'];\n\n $keywordGuide = $keywords ? \"\\n\\nIncorporate these SEO keywords naturally: {$keywords}\" : '';\n\n return \"Write a comprehensive blog post about: {$topic}\n\nREQUIREMENTS:\n- Word count: {$wordCount} words\n- {$toneGuide}\n- Use proper HTML formatting with <h2>, <h3>, <p>, <ul>, <li>, <strong>, <em> tags\n- Include an engaging introduction that hooks the reader\n- Use subheadings (h2, h3) to organize content\n- Include practical examples or tips where relevant\n- End with a conclusion or call-to-action\n- Make it original, valuable, and SEO-friendly{$keywordGuide}\n\nAfter the blog content, provide on separate lines:\nTITLE: [engaging title under 70 chars]\nEXCERPT: [compelling excerpt under 200 chars]\nMETA_DESC: [SEO meta description under 160 chars]\nTAGS: [5-8 relevant tags, comma separated]\n\nWrite the blog post now:\";\n }\n\n /**\n * Parse generated content\n */\n private function parseGeneratedContent(string $response, string $fallbackTitle): array\n {\n $result = [\n 'title' => $fallbackTitle,\n 'content' => '',\n 'excerpt' => '',\n 'meta_description' => '',\n 'tags' => '',\n ];\n\n // Try to extract metadata from the end\n $lines = explode(\"\\n\", $response);\n $contentLines = [];\n $metadataStarted = false;\n\n foreach ($lines as $line) {\n $line = trim($line);\n\n if (preg_match('/^TITLE:\\s*(.+)$/i', $line, $matches)) {\n $result['title'] = trim($matches[1]);\n $metadataStarted = true;\n } elseif (preg_match('/^EXCERPT:\\s*(.+)$/i', $line, $matches)) {\n $result['excerpt'] = trim($matches[1]);\n $metadataStarted = true;\n } elseif (preg_match('/^META_DESC:\\s*(.+)$/i', $line, $matches)) {\n $result['meta_description'] = trim($matches[1]);\n $metadataStarted = true;\n } elseif (preg_match('/^TAGS:\\s*(.+)$/i', $line, $matches)) {\n $result['tags'] = trim($matches[1]);\n $metadataStarted = true;\n } elseif (!$metadataStarted) {\n $contentLines[] = $line;\n }\n }\n\n $content = implode(\"\\n\", $contentLines);\n $result['content'] = $this->cleanContent($content);\n\n // Generate excerpt if not provided\n if (empty($result['excerpt']) && !empty($result['content'])) {\n $text = strip_tags($result['content']);\n $result['excerpt'] = substr($text, 0, 200);\n if (strlen($text) > 200) {\n $result['excerpt'] = substr($result['excerpt'], 0, strrpos($result['excerpt'], ' ')) . '...';\n }\n }\n\n return $result;\n }\n\n /**\n * Clean generated content\n */\n private function cleanContent(string $content): string\n {\n // Remove markdown code blocks if present\n $content = preg_replace('/```html?\\s*/i', '', $content);\n $content = preg_replace('/```\\s*/', '', $content);\n\n // Ensure proper HTML structure\n $content = trim($content);\n\n // Convert markdown-style headings if present\n $content = preg_replace('/^##\\s+(.+)$/m', '<h2>$1</h2>', $content);\n $content = preg_replace('/^###\\s+(.+)$/m', '<h3>$1</h3>', $content);\n\n // Convert markdown bold/italic if present\n $content = preg_replace('/\\*\\*(.+?)\\*\\*/', '<strong>$1</strong>', $content);\n $content = preg_replace('/\\*(.+?)\\*/', '<em>$1</em>', $content);\n\n // Wrap plain paragraphs\n $lines = explode(\"\\n\\n\", $content);\n $processed = [];\n\n foreach ($lines as $line) {\n $line = trim($line);\n if (empty($line)) continue;\n\n // Skip if already has block-level tags\n if (preg_match('/^<(h[1-6]|p|ul|ol|li|div|blockquote)/i', $line)) {\n $processed[] = $line;\n } else {\n $processed[] = '<p>' . $line . '</p>';\n }\n }\n\n return implode(\"\\n\\n\", $processed);\n }\n\n /**\n * Call the AI API\n */\n private function callAPI(string $prompt): string\n {\n if (empty($this->apiKey)) {\n throw new \\Exception('AI API key is not configured');\n }\n\n $payload = [\n 'model' => $this->model,\n 'messages' => [\n [\n 'role' => 'system',\n 'content' => 'You are an expert blog writer and content strategist. You create engaging, well-structured, SEO-optimized content.',\n ],\n [\n 'role' => 'user',\n 'content' => $prompt,\n ],\n ],\n 'temperature' => 0.7,\n 'max_tokens' => 4000,\n ];\n\n $ch = curl_init($this->endpoint . '/chat/completions');\n curl_setopt_array($ch, [\n CURLOPT_RETURNTRANSFER => true,\n CURLOPT_POST => true,\n CURLOPT_POSTFIELDS => json_encode($payload),\n CURLOPT_HTTPHEADER => [\n 'Content-Type: application/json',\n 'Authorization: Bearer ' . $this->apiKey,\n ],\n CURLOPT_TIMEOUT => 120,\n ]);\n\n $response = curl_exec($ch);\n $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);\n $error = curl_error($ch);\n curl_close($ch);\n\n if ($error) {\n throw new \\Exception('API request failed: ' . $error);\n }\n\n if ($httpCode !== 200) {\n $errorData = json_decode($response, true);\n throw new \\Exception('API error: ' . ($errorData['error']['message'] ?? 'Unknown error'));\n }\n\n $data = json_decode($response, true);\n\n if (!isset($data['choices'][0]['message']['content'])) {\n throw new \\Exception('Invalid API response format');\n }\n\n return $data['choices'][0]['message']['content'];\n }\n}\n"}, "472aa5764620259e9291ed099da1a87f7bbf0f80": {"t": 1785760170.6794844, "n": 10838, "h": "3a34e1f53e91c405", "c": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Services;\n\n/**\n * Blog AI Service\n *\n * Handles AI-powered content generation for blog posts.\n */\nclass BlogAIService\n{\n private string $apiKey;\n private string $endpoint;\n private string $model;\n\n public function __construct()\n {\n $this->apiKey = config('api.deepseek.key', '');\n $this->endpoint = config('api.deepseek.endpoint', 'https://api.deepseek.com');\n $this->model = config('api.deepseek.model', 'deepseek-chat');\n }\n\n /**\n * Generate a complete blog post\n */\n public function generatePost(string $topic, string $tone = 'professional', string $length = 'medium', string $keywords = ''): array\n {\n $wordCounts = [\n 'short' => '500-700',\n 'medium' => '1000-1200',\n 'long' => '1800-2200',\n ];\n\n $targetWords = $wordCounts[$length] ?? '1000-1200';\n\n $prompt = $this->buildGenerationPrompt($topic, $tone, $targetWords, $keywords);\n\n $response = $this->callAPI($prompt);\n\n return $this->parseGeneratedContent($response, $topic);\n }\n\n /**\n * Improve existing content\n */\n public function improveContent(string $content, string $action = 'improve'): array\n {\n $prompts = [\n 'improve' => \"Improve the following blog content. Make it more engaging, clear, and well-structured while maintaining the original message. Fix any grammatical errors and improve the flow:\\n\\n{$content}\\n\\nProvide only the improved content, no explanations.\",\n\n 'expand' => \"Expand the following content with more details, examples, and explanations. Add relevant information to make it more comprehensive. Double the length while keeping it focused:\\n\\n{$content}\\n\\nProvide only the expanded content.\",\n\n 'summarize' => \"Summarize the following content into a concise version that retains the key points. Reduce it to about 30% of the original length:\\n\\n{$content}\\n\\nProvide only the summarized content.\",\n\n 'seo' => \"Optimize the following content for SEO. Add relevant keywords naturally, improve headings for better structure, and make it more search-engine friendly while maintaining readability:\\n\\n{$content}\\n\\nProvide only the optimized content.\",\n ];\n\n $prompt = $prompts[$action] ?? $prompts['improve'];\n\n $response = $this->callAPI($prompt);\n\n return [\n 'content' => $this->cleanContent($response),\n ];\n }\n\n /**\n * Generate SEO metadata\n */\n public function generateMetadata(string $title, string $content): array\n {\n $prompt = \"Based on this blog post, generate SEO metadata:\n\nTitle: {$title}\nContent (excerpt): \" . substr(strip_tags($content), 0, 500) . \"\n\nProvide in this exact JSON format:\n{\n \\\"meta_title\\\": \\\"SEO optimized title under 60 chars\\\",\n \\\"meta_description\\\": \\\"Compelling meta description under 160 chars\\\",\n \\\"keywords\\\": \\\"keyword1, keyword2, keyword3, keyword4, keyword5\\\"\n}\n\nReturn ONLY the JSON, no other text.\";\n\n $response = $this->callAPI($prompt);\n\n try {\n $data = json_decode($response, true);\n if ($data) {\n return $data;\n }\n } catch (\\Exception $e) {\n // Fall through to defaults\n }\n\n return [\n 'meta_title' => substr($title, 0, 60),\n 'meta_description' => substr(strip_tags($content), 0, 160),\n 'keywords' => '',\n ];\n }\n\n /**\n * Generate tags from content\n */\n public function generateTags(string $title, string $content): array\n {\n $prompt = \"Analyze this blog post and suggest 5-8 relevant tags (single words or short phrases).\n\nTitle: {$title}\nContent: \" . substr(strip_tags($content), 0, 1000) . \"\n\nReturn ONLY a comma-separated list of tags, nothing else. Example: technology, artificial intelligence, machine learning, data science\";\n\n $response = $this->callAPI($prompt);\n\n $tags = array_filter(array_map('trim', explode(',', $response)));\n return array_slice($tags, 0, 8);\n }\n\n /**\n * Build generation prompt\n */\n private function buildGenerationPrompt(string $topic, string $tone, string $wordCount, string $keywords): string\n {\n $toneInstructions = [\n 'professional' => 'Use a professional, authoritative tone. Be informative and credible.',\n 'casual' => 'Use a casual, conversational tone. Be friendly and approachable.',\n 'friendly' => 'Use a warm, friendly tone. Be encouraging and supportive.',\n 'authoritative' => 'Use an expert, authoritative tone. Demonstrate deep knowledge.',\n 'humorous' => 'Use a light, humorous tone where appropriate. Keep it engaging and fun.',\n ];\n\n $toneGuide = $toneInstructions[$tone] ?? $toneInstructions['professional'];\n\n $keywordGuide = $keywords ? \"\\n\\nIncorporate these SEO keywords naturally: {$keywords}\" : '';\n\n return \"Write a comprehensive blog post about: {$topic}\n\nREQUIREMENTS:\n- Word count: {$wordCount} words\n- {$toneGuide}\n- Use proper HTML formatting with <h2>, <h3>, <p>, <ul>, <li>, <strong>, <em> tags\n- Include an engaging introduction that hooks the reader\n- Use subheadings (h2, h3) to organize content\n- Include practical examples or tips where relevant\n- End with a conclusion or call-to-action\n- Make it original, valuable, and SEO-friendly{$keywordGuide}\n\nAfter the blog content, provide on separate lines:\nTITLE: [engaging title under 70 chars]\nEXCERPT: [compelling excerpt under 200 chars]\nMETA_DESC: [SEO meta description under 160 chars]\nTAGS: [5-8 relevant tags, comma separated]\n\nWrite the blog post now:\";\n }\n\n /**\n * Parse generated content\n */\n private function parseGeneratedContent(string $response, string $fallbackTitle): array\n {\n $result = [\n 'title' => $fallbackTitle,\n 'content' => '',\n 'excerpt' => '',\n 'meta_description' => '',\n 'tags' => '',\n ];\n\n // Try to extract metadata from the end\n $lines = explode(\"\\n\", $response);\n $contentLines = [];\n $metadataStarted = false;\n\n foreach ($lines as $line) {\n $line = trim($line);\n\n if (preg_match('/^TITLE:\\s*(.+)$/i', $line, $matches)) {\n $result['title'] = trim($matches[1]);\n $metadataStarted = true;\n } elseif (preg_match('/^EXCERPT:\\s*(.+)$/i', $line, $matches)) {\n $result['excerpt'] = trim($matches[1]);\n $metadataStarted = true;\n } elseif (preg_match('/^META_DESC:\\s*(.+)$/i', $line, $matches)) {\n $result['meta_description'] = trim($matches[1]);\n $metadataStarted = true;\n } elseif (preg_match('/^TAGS:\\s*(.+)$/i', $line, $matches)) {\n $result['tags'] = trim($matches[1]);\n $metadataStarted = true;\n } elseif (!$metadataStarted) {\n $contentLines[] = $line;\n }\n }\n\n $content = implode(\"\\n\", $contentLines);\n $result['content'] = $this->cleanContent($content);\n\n // Generate excerpt if not provided\n if (empty($result['excerpt']) && !empty($result['content'])) {\n $text = strip_tags($result['content']);\n $result['excerpt'] = substr($text, 0, 200);\n if (strlen($text) > 200) {\n $result['excerpt'] = substr($result['excerpt'], 0, strrpos($result['excerpt'], ' ')) . '...';\n }\n }\n\n return $result;\n }\n\n /**\n * Clean generated content\n */\n private function cleanContent(string $content): string\n {\n // Remove markdown code blocks if present\n $content = preg_replace('/```html?\\s*/i', '', $content);\n $content = preg_replace('/```\\s*/', '', $content);\n\n // Ensure proper HTML structure\n $content = trim($content);\n\n // Convert markdown-style headings if present\n $content = preg_replace('/^##\\s+(.+)$/m', '<h2>$1</h2>', $content);\n $content = preg_replace('/^###\\s+(.+)$/m', '<h3>$1</h3>', $content);\n\n // Convert markdown bold/italic if present\n $content = preg_replace('/\\*\\*(.+?)\\*\\*/', '<strong>$1</strong>', $content);\n $content = preg_replace('/\\*(.+?)\\*/', '<em>$1</em>', $content);\n\n // Wrap plain paragraphs\n $lines = explode(\"\\n\\n\", $content);\n $processed = [];\n\n foreach ($lines as $line) {\n $line = trim($line);\n if (empty($line)) continue;\n\n // Skip if already has block-level tags\n if (preg_match('/^<(h[1-6]|p|ul|ol|li|div|blockquote)/i', $line)) {\n $processed[] = $line;\n } else {\n $processed[] = '<p>' . $line . '</p>';\n }\n }\n\n return implode(\"\\n\\n\", $processed);\n }\n\n /**\n * Call the AI API\n */\n private function callAPI(string $prompt): string\n {\n if (empty($this->apiKey)) {\n throw new \\Exception('AI API key is not configured');\n }\n\n $payload = [\n 'model' => $this->model,\n 'messages' => [\n [\n 'role' => 'system',\n 'content' => 'You are an expert blog writer and content strategist. You create engaging, well-structured, SEO-optimized content.',\n ],\n [\n 'role' => 'user',\n 'content' => $prompt,\n ],\n ],\n 'temperature' => 0.7,\n 'max_tokens' => 4000,\n ];\n\n $ch = curl_init($this->endpoint . '/chat/completions');\n curl_setopt_array($ch, [\n CURLOPT_RETURNTRANSFER => true,\n CURLOPT_POST => true,\n CURLOPT_POSTFIELDS => json_encode($payload),\n CURLOPT_HTTPHEADER => [\n 'Content-Type: application/json',\n 'Authorization: Bearer ' . $this->apiKey,\n ],\n CURLOPT_TIMEOUT => 120,\n ]);\n\n $response = curl_exec($ch);\n $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);\n $error = curl_error($ch);\n curl_close($ch);\n\n if ($error) {\n throw new \\Exception('API request failed: ' . $error);\n }\n\n if ($httpCode !== 200) {\n $errorData = json_decode($response, true);\n throw new \\Exception('API error: ' . ($errorData['error']['message'] ?? 'Unknown error'));\n }\n\n $data = json_decode($response, true);\n\n if (!isset($data['choices'][0]['message']['content'])) {\n throw new \\Exception('Invalid API response format');\n }\n\n return $data['choices'][0]['message']['content'];\n }\n}\n"}}, "app/Config/config.php": {"99e7b33467817c60024f776bd28554aa74d4d15d": {"t": 1786067613.4631586, "n": 7013, "h": "e0e94a2254093aa0", "c": "<?php\n\ndeclare(strict_types=1);\n\n/**\n * Main Application Configuration\n *\n * All configuration values for the application.\n * For sensitive values, consider using environment variables.\n */\n\n// Load database configuration from separate file if it exists (created by installer)\n$databaseConfig = [];\n$databaseConfigFile = __DIR__ . '/database.php';\nif (file_exists($databaseConfigFile)) {\n $databaseConfig = require $databaseConfigFile;\n} else {\n // Default fallback (used during development or before installation)\n $databaseConfig = [\n 'driver' => 'mysql',\n 'host' => 'localhost',\n 'port' => 3306,\n 'database' => 'fwp_gallery',\n 'username' => 'root',\n 'password' => '',\n 'charset' => 'utf8mb4',\n 'collation' => 'utf8mb4_unicode_ci',\n 'prefix' => '',\n 'options' => [\n PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,\n PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,\n PDO::ATTR_EMULATE_PREPARES => false,\n ],\n ];\n}\n\nreturn [\n /*\n |--------------------------------------------------------------------------\n | Application Settings\n |--------------------------------------------------------------------------\n */\n 'app' => [\n 'name' => 'Pixly',\n 'url' => (function() {\n // Auto-detect URL from request, including subdirectory path\n // Check multiple sources for HTTPS (handles proxies/load balancers)\n $isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')\n || (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https')\n || (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] === 'on')\n || (!empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443);\n $protocol = $isHttps ? 'https' : 'http';\n $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? 'localhost';\n\n // Detect subdirectory path from SCRIPT_NAME\n $basePath = '';\n if (!empty($_SERVER['SCRIPT_NAME'])) {\n $basePath = dirname($_SERVER['SCRIPT_NAME']);\n // Normalize path separators and remove trailing slashes\n $basePath = str_replace('\\\\', '/', $basePath);\n if ($basePath === '/' || $basePath === '.') {\n $basePath = '';\n }\n }\n\n return \"{$protocol}://{$host}{$basePath}\";\n })(),\n 'env' => 'production', // development, staging, production\n 'debug' => false,\n 'timezone' => 'UTC',\n 'locale' => 'en',\n ],\n\n /*\n |--------------------------------------------------------------------------\n | Database Configuration\n |--------------------------------------------------------------------------\n */\n 'database' => $databaseConfig,\n\n /*\n |--------------------------------------------------------------------------\n | Session Configuration\n |--------------------------------------------------------------------------\n */\n 'session' => [\n 'lifetime' => 7200, // 2 hours\n 'secure' => false, // Set to true in production with HTTPS\n 'httponly' => true,\n 'samesite' => 'Lax',\n ],\n\n /*\n |--------------------------------------------------------------------------\n | Security Settings\n |--------------------------------------------------------------------------\n */\n 'security' => [\n 'csrf_token_name' => '_token',\n 'password_min_length' => 12,\n 'login_attempts_limit' => 5,\n 'login_lockout_minutes' => 15,\n ],\n\n /*\n |--------------------------------------------------------------------------\n | Image Settings\n |--------------------------------------------------------------------------\n */\n 'images' => [\n 'max_size' => 10485760, // 10MB\n 'allowed_types' => ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],\n 'max_dimension' => 8000,\n 'quality' => 85,\n 'sizes' => [\n 'thumbnail' => ['width' => 300, 'height' => 300, 'crop' => true],\n 'medium' => ['width' => 800, 'height' => null, 'crop' => false],\n 'large' => ['width' => 1200, 'height' => null, 'crop' => false],\n ],\n ],\n\n /*\n |--------------------------------------------------------------------------\n | Cache Settings\n |--------------------------------------------------------------------------\n */\n 'cache' => [\n 'enabled' => true,\n 'driver' => 'file',\n 'path' => PUBLIC_PATH . '/cache',\n 'default_ttl' => 3600, // 1 hour\n 'page_ttl' => 300, // 5 minutes\n ],\n\n /*\n |--------------------------------------------------------------------------\n | Pagination\n |--------------------------------------------------------------------------\n */\n 'pagination' => [\n 'per_page' => 24,\n 'max_per_page' => 100,\n ],\n\n /*\n |--------------------------------------------------------------------------\n | SEO Settings\n |--------------------------------------------------------------------------\n */\n 'seo' => [\n 'site_title' => 'Pixly',\n 'site_description' => 'Discover trending images, creative visuals, and inspiring content.',\n 'title_separator' => ' | ',\n 'default_image' => '/assets/images/og-default.jpg',\n ],\n\n /*\n |--------------------------------------------------------------------------\n | External API Keys\n |--------------------------------------------------------------------------\n | Store sensitive keys in environment variables for production\n */\n 'api' => [\n 'deepseek' => [\n 'key' => '', // Add your DeepSeek API key\n 'endpoint' => 'https://api.deepseek.com',\n 'model' => 'deepseek-chat',\n 'daily_limit' => 100,\n // For vision, use DeepInfra with Janus-Pro (get free API key at https://deepinfra.com)\n 'vision_endpoint' => 'https://api.deepinfra.com/v1/openai',\n 'vision_model' => 'deepseek-ai/Janus-Pro-7B',\n 'vision_key' => '', // Add your DeepInfra API key here for image analysis\n ],\n 'unsplash' => [\n 'access_key' => '', // Add your Unsplash access key\n 'secret_key' => '',\n ],\n 'pexels' => [\n 'api_key' => '', // Add your Pexels API key\n ],\n ],\n\n /*\n |--------------------------------------------------------------------------\n | Ads Configuration\n |--------------------------------------------------------------------------\n */\n 'ads' => [\n 'enabled' => true,\n 'network' => 'juicyads',\n 'placements' => [\n 'header' => true,\n 'sidebar' => true,\n 'between_images' => true,\n 'footer' => true,\n ],\n 'images_between_ads' => 8,\n ],\n];\n"}}, "app/Controllers/Admin/AIController.php": {"99e7b33467817c60024f776bd28554aa74d4d15d": {"t": 1786068320.3971243, "n": 11694, "h": "9896b914c4d16be5", "c": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Controllers\\Admin;\n\nuse App\\Core\\Controller;\nuse App\\Core\\Response;\nuse App\\Services\\ClaudeAIService;\nuse App\\Services\\ReplicateAIService;\nuse App\\Services\\AIHordeService;\nuse App\\Services\\OpenRouterService;\nuse App\\Services\\AI\\MetadataGenerator;\n\n/**\n * Admin AI Controller\n *\n * Handles AI processing of images.\n */\nclass AIController extends Controller\n{\n /**\n * Show AI processing page\n */\n public function index(): Response\n {\n $db = $this->db();\n\n // Ensure AI settings exist in database\n $this->ensureAISettingsExist($db);\n\n // Clean up: Delete old completed entries (keep queue clean)\n $db->execute(\"DELETE FROM ai_processing_queue WHERE status = 'completed'\");\n\n // Clean up: Remove orphaned queue entries for already-published images\n $db->execute(\n \"DELETE FROM ai_processing_queue\n WHERE image_id IN (\n SELECT id FROM images\n WHERE status = 'published' AND ai_processed_at IS NOT NULL\n )\"\n );\n\n // Reset any stuck \"processing\" entries back to pending (older than 10 min)\n try {\n $db->execute(\n \"UPDATE ai_processing_queue SET status = 'pending'\n WHERE status = 'processing' AND started_at < DATE_SUB(NOW(), INTERVAL 10 MINUTE)\"\n );\n } catch (\\Throwable $e) {\n // Column may not exist yet - ignore, migration will add it\n }\n\n $generator = new MetadataGenerator();\n $provider = $generator->getProvider();\n\n // Check if the current provider is configured\n if ($provider === 'openrouter') {\n $ai = new OpenRouterService();\n } elseif ($provider === 'aihorde') {\n $ai = new AIHordeService();\n } elseif ($provider === 'replicate') {\n $ai = new ReplicateAIService();\n } else {\n $ai = new ClaudeAIService();\n }\n\n // Get IMAGE stats (synced with admin/images)\n $imageStats = [\n 'draft' => (int) $db->fetchColumn(\"SELECT COUNT(*) FROM images WHERE status = 'draft'\"),\n 'published' => (int) $db->fetchColumn(\"SELECT COUNT(*) FROM images WHERE status = 'published'\"),\n 'total' => (int) $db->fetchColumn(\"SELECT COUNT(*) FROM images\"),\n ];\n\n // Get QUEUE stats (only pending/failed since completed are deleted)\n $queueStats = [\n 'pending' => (int) $db->fetchColumn(\"SELECT COUNT(*) FROM ai_processing_queue WHERE status = 'pending'\"),\n 'processing' => (int) $db->fetchColumn(\"SELECT COUNT(*) FROM ai_processing_queue WHERE status = 'processing'\"),\n 'failed' => (int) $db->fetchColumn(\"SELECT COUNT(*) FROM ai_processing_queue WHERE status = 'failed'\"),\n ];\n\n // Get queue items (only pending/processing/failed - no completed)\n $queue = $db->fetchAll(\n \"SELECT q.*, i.title, i.thumbnail_path, i.status as image_status\n FROM ai_processing_queue q\n JOIN images i ON q.image_id = i.id\n WHERE q.status IN ('pending', 'processing', 'failed')\n ORDER BY\n CASE q.status\n WHEN 'processing' THEN 1\n WHEN 'pending' THEN 2\n WHEN 'failed' THEN 3\n END,\n q.priority DESC,\n q.created_at DESC\n LIMIT 100\"\n );\n\n // Get draft images not in queue (need to be queued)\n $unqueued = $db->fetchAll(\n \"SELECT i.id, i.title, i.thumbnail_path, i.created_at, i.status\n FROM images i\n LEFT JOIN ai_processing_queue q ON i.id = q.image_id\n WHERE i.status = 'draft' AND q.id IS NULL\n ORDER BY i.created_at DESC\n LIMIT 50\"\n );\n\n return $this->view('admin/ai/index', [\n 'title' => 'AI Processing',\n 'currentPage' => 'ai',\n 'isConfigured' => $ai->isConfigured(),\n 'provider' => $provider,\n 'providerName' => match($provider) {\n 'openrouter' => 'OpenRouter (Qwen 2.5 VL)',\n 'aihorde' => 'AI Horde (Caption + Tags)',\n 'replicate' => 'Replicate (LLaVA)',\n default => 'Claude',\n },\n 'imageStats' => $imageStats,\n 'queueStats' => $queueStats,\n 'queue' => $queue,\n 'unqueued' => $unqueued,\n ], 'admin');\n }\n\n /**\n * Process queued images\n */\n public function process(): Response\n {\n $generator = new MetadataGenerator();\n\n $limit = (int) $this->request->input('limit', 5);\n $limit = min($limit, 50); // Max 50 at a time\n\n $results = $generator->processQueue($limit);\n\n if ($this->request->isAjax()) {\n return $this->json($results);\n }\n\n if ($results['processed'] > 0) {\n return $this->redirectWithSuccess(\n url('/admin/ai'),\n \"Processed {$results['processed']} image(s). \" .\n ($results['failed'] > 0 ? \"{$results['failed']} failed.\" : '')\n );\n }\n\n return $this->redirectWithError(\n url('/admin/ai'),\n $results['errors'][0] ?? 'No images to process'\n );\n }\n\n /**\n * Process all pending images in queue (batch mode)\n */\n public function processAll(): Response\n {\n $db = $this->db();\n $generator = new MetadataGenerator();\n\n // Get count of pending items\n $pendingCount = (int) $db->fetchColumn(\n \"SELECT COUNT(*) FROM ai_processing_queue WHERE status = 'pending'\"\n );\n\n if ($pendingCount === 0) {\n return $this->redirectWithSuccess(url('/admin/ai'), 'No pending images to process.');\n }\n\n // Process in batches to avoid timeout\n $batchSize = 10;\n $totalProcessed = 0;\n $totalFailed = 0;\n $maxBatches = 5; // Process max 50 images per request\n\n for ($batch = 0; $batch < $maxBatches; $batch++) {\n $results = $generator->processQueue($batchSize);\n $totalProcessed += $results['processed'];\n $totalFailed += $results['failed'];\n\n // Stop if no more items\n if ($results['processed'] === 0 && $results['failed'] === 0) {\n break;\n }\n }\n\n $remaining = $pendingCount - $totalProcessed - $totalFailed;\n\n $message = \"Processed {$totalProcessed} image(s).\";\n if ($totalFailed > 0) {\n $message .= \" {$totalFailed} failed.\";\n }\n if ($remaining > 0) {\n $message .= \" {$remaining} still pending - click again to continue.\";\n }\n\n return $this->redirectWithSuccess(url('/admin/ai'), $message);\n }\n\n /**\n * Add images to processing queue\n */\n public function queue(): Response\n {\n $db = $this->db();\n $imageIds = $this->request->input('image_ids', []);\n\n if (empty($imageIds)) {\n // Queue all unprocessed images\n $unprocessed = $db->fetchAll(\n \"SELECT i.id FROM images i\n LEFT JOIN ai_processing_queue q ON i.id = q.image_id\n WHERE i.ai_processed_at IS NULL AND q.id IS NULL\n LIMIT 50\"\n );\n $imageIds = array_column($unprocessed, 'id');\n }\n\n if (empty($imageIds)) {\n return $this->redirectWithError(url('/admin/ai'), 'No images to queue');\n }\n\n $queued = 0;\n foreach ($imageIds as $imageId) {\n // Check if already in queue\n $existing = $db->fetch(\n \"SELECT id FROM ai_processing_queue WHERE image_id = :id AND status IN ('pending', 'processing')\",\n ['id' => $imageId]\n );\n\n if (!$existing) {\n $db->insert('ai_processing_queue', [\n 'image_id' => (int) $imageId,\n 'task_type' => 'all',\n 'priority' => 5,\n 'status' => 'pending',\n ]);\n $queued++;\n }\n }\n\n return $this->redirectWithSuccess(\n url('/admin/ai'),\n \"Added {$queued} image(s) to the processing queue.\"\n );\n }\n\n /**\n * Process a single image immediately\n */\n public function processSingle(string|int $id): Response\n {\n $id = (int) $id;\n $generator = new MetadataGenerator();\n\n $success = $generator->processImage($id);\n\n if ($this->request->isAjax()) {\n if ($success) {\n return $this->json(['success' => true]);\n }\n return $this->json(['success' => false, 'errors' => $generator->getErrors()], 400);\n }\n\n if ($success) {\n return $this->redirectWithSuccess(url('/admin/ai'), 'Image processed successfully.');\n }\n\n return $this->redirectWithError(\n url('/admin/ai'),\n 'Processing failed: ' . implode(', ', $generator->getErrors())\n );\n }\n\n /**\n * Clear failed items from queue\n */\n public function clearFailed(): Response\n {\n $db = $this->db();\n $db->execute(\"DELETE FROM ai_processing_queue WHERE status = 'failed'\");\n\n return $this->redirectWithSuccess(url('/admin/ai'), 'Cleared failed items from queue.');\n }\n\n /**\n * Retry failed items\n */\n public function retryFailed(): Response\n {\n $db = $this->db();\n $db->execute(\n \"UPDATE ai_processing_queue SET status = 'pending', attempts = 0, error_message = NULL WHERE status = 'failed'\"\n );\n\n return $this->redirectWithSuccess(url('/admin/ai'), 'Failed items queued for retry.');\n }\n\n /**\n * Ensure all AI-related settings exist in database\n */\n private function ensureAISettingsExist($db): void\n {\n // Remove old unused API keys\n $db->execute(\"DELETE FROM settings WHERE setting_key IN ('deepseek_api_key', 'deepinfra_api_key')\");\n\n $requiredSettings = [\n [\n 'setting_key' => 'ai_provider',\n 'setting_value' => 'huggingface',\n 'setting_type' => 'string',\n 'description' => 'AI provider (huggingface, replicate, or claude)',\n 'is_public' => 0,\n ],\n [\n 'setting_key' => 'huggingface_api_key',\n 'setting_value' => '',\n 'setting_type' => 'encrypted',\n 'description' => 'Hugging Face API key for Florence-2 + WD14 (recommended for adult sites)',\n 'is_public' => 0,\n ],\n [\n 'setting_key' => 'replicate_api_key',\n 'setting_value' => '',\n 'setting_type' => 'encrypted',\n 'description' => 'Replicate API key for LLaVA image analysis',\n 'is_public' => 0,\n ],\n [\n 'setting_key' => 'claude_api_key',\n 'setting_value' => '',\n 'setting_type' => 'encrypted',\n 'description' => 'Claude API key for image analysis',\n 'is_public' => 0,\n ],\n ];\n\n foreach ($requiredSettings as $setting) {\n $exists = $db->fetch(\n \"SELECT 1 FROM settings WHERE setting_key = :key\",\n ['key' => $setting['setting_key']]\n );\n\n if (!$exists) {\n $db->insert('settings', $setting);\n }\n }\n }\n}\n"}}}} |