From 2a4bf8b58bd2e70943c9c0956624a494362ac09f Mon Sep 17 00:00:00 2001 From: "Khoa.vo" Date: Tue, 6 Jan 2026 13:25:58 +0700 Subject: [PATCH] feat: updates before deployment --- .gitignore | 5 + app/api/generate/route.ts | 14 +- app/api/grok-chat/route.ts | 51 + app/api/grok-debug/route.ts | 54 + app/api/meta-crawl/route.ts | 130 + app/page.tsx | 7 + components/CookieExpiredDialog.tsx | 80 + components/Gallery.tsx | 97 +- components/GrokChat.tsx | 243 ++ components/Navbar.tsx | 115 +- components/PromptHero.tsx | 189 +- components/Settings.tsx | 2 +- data/prompts.json | 2397 +++++++++-------- docker-compose.yml | 17 + lib/db.ts | 3 +- lib/providers/meta-crawl-client.ts | 174 ++ lib/store.ts | 12 + services/crawl4ai/Dockerfile | 38 + services/crawl4ai/README.md | 91 + services/crawl4ai/app/__init__.py | 1 + services/crawl4ai/app/config.py | 21 + services/crawl4ai/app/grok/__init__.py | 7 + services/crawl4ai/app/grok/grok.py | 328 +++ services/crawl4ai/app/grok/headers.py | 78 + services/crawl4ai/app/grok/logger.py | 60 + services/crawl4ai/app/grok/mappings/grok.json | 65 + services/crawl4ai/app/grok/mappings/txid.json | 1 + services/crawl4ai/app/grok/reverse/anon.py | 43 + services/crawl4ai/app/grok/reverse/parser.py | 139 + services/crawl4ai/app/grok/reverse/xctid.py | 180 ++ services/crawl4ai/app/grok/runtime.py | 49 + services/crawl4ai/app/grok_auth.py | 111 + services/crawl4ai/app/grok_client.py | 98 + services/crawl4ai/app/main.py | 184 ++ services/crawl4ai/app/meta_crawler.py | 189 ++ services/crawl4ai/app/models.py | 66 + services/crawl4ai/requirements.txt | 24 + 37 files changed, 4024 insertions(+), 1339 deletions(-) create mode 100644 app/api/grok-chat/route.ts create mode 100644 app/api/grok-debug/route.ts create mode 100644 app/api/meta-crawl/route.ts create mode 100644 components/CookieExpiredDialog.tsx create mode 100644 components/GrokChat.tsx create mode 100644 lib/providers/meta-crawl-client.ts create mode 100644 services/crawl4ai/Dockerfile create mode 100644 services/crawl4ai/README.md create mode 100644 services/crawl4ai/app/__init__.py create mode 100644 services/crawl4ai/app/config.py create mode 100644 services/crawl4ai/app/grok/__init__.py create mode 100644 services/crawl4ai/app/grok/grok.py create mode 100644 services/crawl4ai/app/grok/headers.py create mode 100644 services/crawl4ai/app/grok/logger.py create mode 100644 services/crawl4ai/app/grok/mappings/grok.json create mode 100644 services/crawl4ai/app/grok/mappings/txid.json create mode 100644 services/crawl4ai/app/grok/reverse/anon.py create mode 100644 services/crawl4ai/app/grok/reverse/parser.py create mode 100644 services/crawl4ai/app/grok/reverse/xctid.py create mode 100644 services/crawl4ai/app/grok/runtime.py create mode 100644 services/crawl4ai/app/grok_auth.py create mode 100644 services/crawl4ai/app/grok_client.py create mode 100644 services/crawl4ai/app/main.py create mode 100644 services/crawl4ai/app/meta_crawler.py create mode 100644 services/crawl4ai/app/models.py create mode 100644 services/crawl4ai/requirements.txt diff --git a/.gitignore b/.gitignore index db3aff8..486b0ec 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,8 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# local env +.venv +error.log +__pycache__ diff --git a/app/api/generate/route.ts b/app/api/generate/route.ts index eaffe00..512d13b 100644 --- a/app/api/generate/route.ts +++ b/app/api/generate/route.ts @@ -62,10 +62,20 @@ export async function POST(req: NextRequest) { return NextResponse.json({ images }); } catch (error: any) { - console.error("Generate API Error:", error); + console.error("Generate API Error Details:", { + message: error.message, + stack: error.stack, + fullError: error + }); + + const msg = error.message || ""; + const isAuthError = msg.includes("401") || msg.includes("403") || + msg.includes("Auth") || msg.includes("auth") || + msg.includes("cookies") || msg.includes("expired"); + return NextResponse.json( { error: error.message || "Generation failed" }, - { status: 500 } + { status: isAuthError ? 401 : 500 } ); } } diff --git a/app/api/grok-chat/route.ts b/app/api/grok-chat/route.ts new file mode 100644 index 0000000..b863a45 --- /dev/null +++ b/app/api/grok-chat/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const CRAWL_SERVICE_URL = 'http://127.0.0.1:8000'; + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { message, history } = body; + + console.log(`[Grok API] Incoming body:`, JSON.stringify(body, null, 2)); + + const proxyPayload = { + message, + history, + cookies: body.cookies + }; + console.log(`[Grok API] Proxy payload:`, JSON.stringify(proxyPayload, null, 2)); + + const response = await fetch(`${CRAWL_SERVICE_URL}/grok/chat`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(proxyPayload), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`[Grok API] Service error: ${response.status} ${errorText}`); + try { + const errorJson = JSON.parse(errorText); + return NextResponse.json(errorJson, { status: response.status }); + } catch { + return NextResponse.json( + { error: `Service error: ${response.status} - ${errorText}` }, + { status: response.status } + ); + } + } + + const data = await response.json(); + return NextResponse.json(data); + + } catch (error: any) { + console.error('[Grok API] Proxy error:', error); + return NextResponse.json( + { error: error.message || 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/app/api/grok-debug/route.ts b/app/api/grok-debug/route.ts new file mode 100644 index 0000000..7215a74 --- /dev/null +++ b/app/api/grok-debug/route.ts @@ -0,0 +1,54 @@ +import { NextRequest, NextResponse } from 'next/server'; + +const CRAWL_SERVICE_URL = 'http://127.0.0.1:8000'; + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { message, history } = body; + + console.log(`[Grok Debug API] Incoming body:`, JSON.stringify(body, null, 2)); + + const proxyPayload = { + message, + history: history || [], + cookies: body.cookies || null, + user_agent: body.userAgent || null + }; + console.log(`[Grok Debug API] Proxy payload:`, JSON.stringify(proxyPayload, null, 2)); + + const response = await fetch(`${CRAWL_SERVICE_URL}/grok/chat`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(proxyPayload), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`[Grok Debug API] Service error: ${response.status} ${errorText}`); + try { + // Try to parse detailed JSON error from FastAPI + const errorJson = JSON.parse(errorText); + return NextResponse.json(errorJson, { status: response.status }); + } catch { + // Fallback to text + return NextResponse.json( + { error: `Service error: ${response.status} - ${errorText}` }, + { status: response.status } + ); + } + } + + const data = await response.json(); + return NextResponse.json(data); + + } catch (error: any) { + console.error('[Grok Debug API] Internal error:', error); + return NextResponse.json( + { error: error.message || 'Internal Server Error' }, + { status: 500 } + ); + } +} diff --git a/app/api/meta-crawl/route.ts b/app/api/meta-crawl/route.ts new file mode 100644 index 0000000..b113d5e --- /dev/null +++ b/app/api/meta-crawl/route.ts @@ -0,0 +1,130 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { MetaCrawlClient } from '@/lib/providers/meta-crawl-client'; + +/** + * API Route: /api/meta-crawl + * + * Proxies image generation requests to the Crawl4AI Python service + * which uses browser automation to interact with Meta AI. + */ + +const client = new MetaCrawlClient(); + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + // Support both numImages (camelCase) and num_images (snake_case) + const { prompt, cookies, numImages, num_images, async = false } = body; + const imageCount = num_images || numImages || 4; + + if (!prompt) { + return NextResponse.json( + { error: "Prompt is required" }, + { status: 400 } + ); + } + + if (!cookies) { + return NextResponse.json( + { error: "Meta AI cookies are required. Please configure in settings." }, + { status: 401 } + ); + } + + // Check if service is healthy + const isHealthy = await client.healthCheck(); + if (!isHealthy) { + return NextResponse.json( + { error: "Crawl4AI service is not available. Please try again later." }, + { status: 503 } + ); + } + + if (async) { + // Async mode: return task_id for polling + const taskId = await client.generateAsync(prompt, cookies, imageCount); + return NextResponse.json({ + success: true, + task_id: taskId + }); + } + + // Sync mode: wait for completion + console.log(`[MetaCrawl API] Generating images for: "${prompt.substring(0, 50)}..."`); + + const images = await client.generate(prompt, cookies, imageCount); + + return NextResponse.json({ + success: true, + images: images.map(img => ({ + url: img.url, + data: img.data, + prompt: img.prompt, + model: img.model + })) + }); + + } catch (error: any) { + console.error("[MetaCrawl API] Error:", error); + return NextResponse.json( + { error: error.message || "Image generation failed" }, + { status: 500 } + ); + } +} + +/** + * GET /api/meta-crawl?task_id=xxx + * + * Get status of an async generation task + */ +export async function GET(req: NextRequest) { + const taskId = req.nextUrl.searchParams.get('task_id'); + + if (!taskId) { + // Return rate limit status + try { + const status = await client.getRateLimitStatus(); + return NextResponse.json(status); + } catch { + return NextResponse.json({ error: "Service not available" }, { status: 503 }); + } + } + + try { + const status = await client.getTaskStatus(taskId); + return NextResponse.json(status); + } catch (error: any) { + return NextResponse.json( + { error: error.message }, + { status: error.message === 'Task not found' ? 404 : 500 } + ); + } +} + +/** + * DELETE /api/meta-crawl?task_id=xxx + * + * Clean up a completed task + */ +export async function DELETE(req: NextRequest) { + const taskId = req.nextUrl.searchParams.get('task_id'); + + if (!taskId) { + return NextResponse.json({ error: "task_id is required" }, { status: 400 }); + } + + try { + const response = await fetch(`${process.env.CRAWL4AI_URL || 'http://localhost:8000'}/status/${taskId}`, { + method: 'DELETE' + }); + + if (!response.ok) { + return NextResponse.json({ error: "Failed to delete task" }, { status: response.status }); + } + + return NextResponse.json({ deleted: true }); + } catch (error: any) { + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/app/page.tsx b/app/page.tsx index 24bf719..bd1fad9 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -9,6 +9,9 @@ import { PromptHero } from "@/components/PromptHero"; import { Settings } from "@/components/Settings"; import { PromptLibrary } from "@/components/PromptLibrary"; import { UploadHistory } from "@/components/UploadHistory"; +import { GrokChat } from "@/components/GrokChat"; +import { CookieExpiredDialog } from "@/components/CookieExpiredDialog"; + export default function Home() { const { currentView, setCurrentView, loadGallery } = useStore(); @@ -48,6 +51,10 @@ export default function Home() { + + {/* Floating Chat */} + {/* */} + ); } diff --git a/components/CookieExpiredDialog.tsx b/components/CookieExpiredDialog.tsx new file mode 100644 index 0000000..01d897d --- /dev/null +++ b/components/CookieExpiredDialog.tsx @@ -0,0 +1,80 @@ +"use client"; + +import React from 'react'; +import { useStore } from '@/lib/store'; +import { AlertTriangle, Settings, X, Cookie, ExternalLink } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +export function CookieExpiredDialog() { + const { + showCookieExpired, + setShowCookieExpired, + setCurrentView, + settings + } = useStore(); + + if (!showCookieExpired) return null; + + const providerName = settings.provider === 'meta' ? 'Meta AI' : + settings.provider === 'grok' ? 'Grok' : + 'Google Whisk'; + + const providerUrl = settings.provider === 'meta' ? 'https://www.meta.ai' : + settings.provider === 'grok' ? 'https://grok.com' : + 'https://labs.google/fx/tools/whisk/project'; + + const handleFixIssues = () => { + setShowCookieExpired(false); + setCurrentView('settings'); + }; + + return ( +
+
+ + {/* Decorative header background */} +
+ +
+ + +
+ +
+ +

Cookies Expired

+ +

+ Your {providerName} session has timed out. + To continue generating images, please refresh your cookies. +

+ +
+ + + + + Open {providerName} + +
+
+
+
+ ); +} diff --git a/components/Gallery.tsx b/components/Gallery.tsx index 1c09ec3..c4d8f78 100644 --- a/components/Gallery.tsx +++ b/components/Gallery.tsx @@ -2,14 +2,25 @@ import React from 'react'; import { useStore } from '@/lib/store'; +import { cn } from "@/lib/utils"; import { motion, AnimatePresence } from 'framer-motion'; import { Download, Maximize2, Sparkles, Trash2, X, ChevronLeft, ChevronRight, Copy, Film, Wand2 } from 'lucide-react'; import { VideoPromptModal } from './VideoPromptModal'; import { EditPromptModal } from './EditPromptModal'; +// Helper function to get proper image src (handles URLs vs base64) +const getImageSrc = (data: string): string => { + if (!data) return ''; + // If it's already a URL, use it directly + if (data.startsWith('http://') || data.startsWith('https://') || data.startsWith('data:')) { + return data; + } + // Otherwise, treat as base64 + return `data:image/png;base64,${data}`; +}; export function Gallery() { - const { gallery, clearGallery, removeFromGallery, setPrompt, addVideo, addToGallery, settings, videos, removeVideo } = useStore(); + const { gallery, clearGallery, removeFromGallery, setPrompt, addVideo, addToGallery, settings, videos, removeVideo, isGenerating } = useStore(); const [selectedIndex, setSelectedIndex] = React.useState(null); const [videoModalOpen, setVideoModalOpen] = React.useState(false); const [videoSource, setVideoSource] = React.useState<{ data: string, prompt: string } | null>(null); @@ -70,6 +81,8 @@ export function Gallery() { errorMessage = '🚫 Content Policy: Video blocked because the image contains a recognizable person. Try using a different image.'; } else if (data.error?.includes('safety') || data.error?.includes('SAFETY')) { errorMessage = '⚠️ Content Policy: Video blocked by Google\'s safety filters. Try a different source image.'; + } else if (data.error?.includes('401') || data.error?.includes('UNAUTHENTICATED')) { + errorMessage = '🔐 Authentication Error: Your Whisk (Google) cookies have expired. Please go to Settings and update them.'; } alert(errorMessage); throw new Error(data.error); @@ -219,6 +232,19 @@ export function Gallery() { {/* Gallery Grid */}
+ {/* Skeleton Loading State */} + {isGenerating && ( + <> + {Array.from({ length: settings.imageCount || 4 }).map((_, i) => ( +
+
+
+
+
+ ))} + + )} + {gallery.map((img, i) => ( {img.prompt} setSelectedIndex(i)} loading="lazy" /> + {/* Provider Tag */} + {img.provider && ( +
+ {img.provider} +
+ )} + {/* Delete button - Top right */} - - {/* Video button - only for 16:9 images */} - {img.aspectRatio === '16:9' ? ( + {/* Remix button - only for Whisk (base64) images for now */} + {(!img.provider || img.provider === 'whisk') && ( + + )} + {/* Video button - only for 16:9 images AND Whisk provider (base64) */} + {img.aspectRatio === '16:9' && (!img.provider || img.provider === 'whisk') ? ( @@ -364,7 +405,7 @@ export function Gallery() { > {selectedImage.prompt} @@ -375,22 +416,24 @@ export function Gallery() {

Download Current - + {(!selectedImage.provider || selectedImage.provider === 'whisk') && ( + + )} + +
+
+ + {/* Messages Area */} + {!isMinimized && ( + <> +
+ {messages.length === 0 && ( +
+ +

Ask Grok anything...

+
+ )} + {messages.map((msg, idx) => ( +
+
+ {msg.content} +
+
+ ))} + {isLoading && ( +
+
+ + Computing... +
+
+ )} +
+
+ + {/* Input Area */} +
+
+ setInput(e.target.value)} + onKeyDown={handleKeyDown} + placeholder="Type a message..." + className="flex-1 bg-black/50 border border-white/10 rounded-xl px-4 py-2.5 text-sm text-white focus:outline-none focus:border-purple-500/50 focus:ring-1 focus:ring-purple-500/20 transition-all placeholder:text-white/20" + disabled={isLoading} + /> + +
+
+ + )} + + )} + +
+ ); +} diff --git a/components/Navbar.tsx b/components/Navbar.tsx index cccef26..590e9ee 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -12,25 +12,73 @@ export function Navbar() { const navItems = [ { id: 'gallery', label: 'Create', icon: Sparkles }, { id: 'library', label: 'Prompt Library', icon: LayoutGrid }, - { id: 'history', label: 'Uploads', icon: Clock }, // CORRECTED: id should match store ViewType 'history' not 'uploads' + { id: 'history', label: 'Uploads', icon: Clock }, ]; return ( -
- {/* Yellow Accent Line */} -
+ <> +
+ {/* Yellow Accent Line */} +
-
- {/* Logo Area */} -
-
- +
+ {/* Logo Area */} +
+
+ +
+ kv-pix
- kv-pix -
- {/* Center Navigation */} -
+ {/* Center Navigation (Desktop) */} +
+ {navItems.map((item) => ( + + ))} +
+ + {/* Right Actions */} +
+ +
+ +
+
+
+ + {/* Mobile Bottom Navigation */} +
+
{navItems.map((item) => ( ))} -
- - {/* Right Actions */} -
+ {/* Settings Item for Mobile */} -
-
-
+ ); } diff --git a/components/PromptHero.tsx b/components/PromptHero.tsx index c2f71ee..b73988e 100644 --- a/components/PromptHero.tsx +++ b/components/PromptHero.tsx @@ -3,7 +3,7 @@ import React, { useRef, useState, useEffect } from "react"; import { useStore, ReferenceCategory } from "@/lib/store"; import { cn } from "@/lib/utils"; -import { Sparkles, Image as ImageIcon, X, Hash, AlertTriangle, Upload, Zap, Brain } from "lucide-react"; +import { Sparkles, Maximize2, X, Hash, AlertTriangle, Upload, Zap, Brain, Settings, Settings2 } from "lucide-react"; const IMAGE_COUNTS = [1, 2, 4]; @@ -13,10 +13,12 @@ export function PromptHero() { settings, setSettings, references, setReference, addReference, removeReference, clearReferences, setSelectionMode, setCurrentView, - history, setHistory + history, setHistory, + setIsGenerating, // Get global setter + setShowCookieExpired } = useStore(); - const [isGenerating, setIsGenerating] = useState(false); + const [isGenerating, setLocalIsGenerating] = useState(false); const [uploadingRefs, setUploadingRefs] = useState>({}); const [errorNotification, setErrorNotification] = useState<{ message: string; type: 'error' | 'warning' } | null>(null); const textareaRef = useRef(null); @@ -55,6 +57,7 @@ export function PromptHero() { } setIsGenerating(true); + setLocalIsGenerating(true); // Keep local state for button UI try { // Route to the selected provider @@ -74,14 +77,15 @@ export function PromptHero() { }) }); } else if (provider === 'meta') { - // Meta AI - res = await fetch('/api/meta/generate', { + // Meta AI via Python service (metaai-api) + // Meta AI always generates 4 images, hardcode this + res = await fetch('/api/meta-crawl', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: finalPrompt, cookies: settings.metaCookies, - imageCount: settings.imageCount + num_images: 4 // Meta AI always returns 4 images }) }); } else { @@ -121,10 +125,11 @@ export function PromptHero() { // Add images one by one with createdAt for (const img of data.images) { await addToGallery({ - data: img.data, + data: img.data || img.url, // Use URL as fallback (Meta AI returns URLs) prompt: img.prompt, aspectRatio: img.aspectRatio || settings.aspectRatio, - createdAt: Date.now() + createdAt: Date.now(), + provider: provider as 'whisk' | 'grok' | 'meta' }); } } @@ -140,6 +145,12 @@ export function PromptHero() { message: '🚫 Content Policy: The reference image contains a recognizable person. Google blocks generating images of real/famous people. Try using a different reference image without identifiable faces.', type: 'warning' }); + } else if (errorMessage.includes("Oops! I can't generate that image") || + errorMessage.includes("Can I help you imagine something else")) { + setErrorNotification({ + message: '🛡️ Meta AI Safety: The prompt was rejected by Meta AI safety filters. Please try a different prompt.', + type: 'warning' + }); } else if (errorMessage.includes('Safety Filter') || errorMessage.includes('SAFETY_FILTER') || errorMessage.includes('content_policy')) { @@ -168,9 +179,15 @@ export function PromptHero() { }); } else if (errorMessage.includes('401') || errorMessage.includes('Unauthorized') || - errorMessage.includes('cookies not found')) { + errorMessage.includes('cookies not found') || + errorMessage.includes('Auth failed')) { + + // Trigger the new popup + setShowCookieExpired(true); + + // Also show a simplified toast as backup setErrorNotification({ - message: '🔐 Authentication Error: Your Whisk cookies may have expired. Please update them in Settings.', + message: '🔐 Authentication Error: Cookies Refreshed Required', type: 'error' }); } else { @@ -183,6 +200,7 @@ export function PromptHero() { setTimeout(() => setErrorNotification(null), 8000); } finally { setIsGenerating(false); + setLocalIsGenerating(false); } }; @@ -344,11 +362,11 @@ export function PromptHero() { ); return ( -
+
{/* Error/Warning Notification Toast */} {errorNotification && (
- - {/* Header / Title + Provider Toggle */} -
-
-
+
+
+
{settings.provider === 'grok' ? ( - + ) : settings.provider === 'meta' ? ( - + ) : ( - + )}
-

Create & Remix

-

- Powered by - {settings.provider === 'grok' ? 'Grok (xAI)' : - settings.provider === 'meta' ? 'Meta AI' : - 'Google Whisk'} +

+ Create + + by + {settings.provider === 'grok' ? 'Grok' : + settings.provider === 'meta' ? 'Meta AI' : + 'Whisk'} + -

+

{/* Provider Toggle */} -
+
@@ -453,23 +471,23 @@ export function PromptHero() { {/* Input Area */}
-
+