diff --git a/src/lib/auth/cryptidEmailService.ts b/src/lib/auth/cryptidEmailService.ts index 00a1709..d70db11 100644 --- a/src/lib/auth/cryptidEmailService.ts +++ b/src/lib/auth/cryptidEmailService.ts @@ -7,10 +7,21 @@ import * as crypto from './crypto'; // Get the worker API URL based on environment function getApiUrl(): string { - // In development, use the local worker - if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { - return 'http://localhost:5172'; + const hostname = window.location.hostname; + + // In development (localhost, local IPs, Tailscale IPs), use the local worker on same host + const isLocalDev = + hostname === 'localhost' || + hostname === '127.0.0.1' || + hostname.startsWith('192.168.') || + hostname.startsWith('10.') || + hostname.startsWith('172.') || + hostname.startsWith('100.'); // Tailscale + + if (isLocalDev) { + return `http://${hostname}:5172`; } + // In production, use the deployed worker return 'https://jeffemmett-canvas.jeffemmett.workers.dev'; } diff --git a/src/lib/clientConfig.ts b/src/lib/clientConfig.ts index f1277fc..f4c2904 100644 --- a/src/lib/clientConfig.ts +++ b/src/lib/clientConfig.ts @@ -21,6 +21,7 @@ export interface ClientConfig { runpodTextEndpointId?: string runpodWhisperEndpointId?: string ollamaUrl?: string + geminiApiKey?: string } /** @@ -52,6 +53,7 @@ export function getClientConfig(): ClientConfig { runpodTextEndpointId: import.meta.env.VITE_RUNPOD_TEXT_ENDPOINT_ID || import.meta.env.NEXT_PUBLIC_RUNPOD_TEXT_ENDPOINT_ID, runpodWhisperEndpointId: import.meta.env.VITE_RUNPOD_WHISPER_ENDPOINT_ID || import.meta.env.NEXT_PUBLIC_RUNPOD_WHISPER_ENDPOINT_ID, ollamaUrl: import.meta.env.VITE_OLLAMA_URL || import.meta.env.NEXT_PUBLIC_OLLAMA_URL, + geminiApiKey: import.meta.env.VITE_GEMINI_API_KEY || import.meta.env.NEXT_PUBLIC_GEMINI_API_KEY, } } else { // Next.js environment @@ -89,6 +91,7 @@ export function getClientConfig(): ClientConfig { runpodTextEndpointId: process.env.VITE_RUNPOD_TEXT_ENDPOINT_ID || process.env.NEXT_PUBLIC_RUNPOD_TEXT_ENDPOINT_ID, runpodWhisperEndpointId: process.env.VITE_RUNPOD_WHISPER_ENDPOINT_ID || process.env.NEXT_PUBLIC_RUNPOD_WHISPER_ENDPOINT_ID, ollamaUrl: process.env.VITE_OLLAMA_URL || process.env.NEXT_PUBLIC_OLLAMA_URL, + geminiApiKey: process.env.VITE_GEMINI_API_KEY || process.env.NEXT_PUBLIC_GEMINI_API_KEY, } } } @@ -328,3 +331,88 @@ export function getOpenAIConfig(): { apiKey: string } | null { return null } } + +/** + * Check if Gemini integration is configured + * Reads from user profile settings (localStorage) or environment variables + */ +export function isGeminiConfigured(): boolean { + try { + // First try to get user-specific API keys if available + const session = JSON.parse(localStorage.getItem('session') || '{}') + if (session.authed && session.username) { + const userApiKeys = localStorage.getItem(`${session.username}_api_keys`) + if (userApiKeys) { + try { + const parsed = JSON.parse(userApiKeys) + if (parsed.keys && parsed.keys.gemini && parsed.keys.gemini.trim() !== '') { + return true + } + } catch (e) { + // Continue to fallback + } + } + } + + // Fallback to global API keys + const settings = localStorage.getItem("gemini_api_key") + if (settings && settings.trim() !== '') { + return true + } + + // Check environment variable + const config = getClientConfig() + if (config.geminiApiKey && config.geminiApiKey.trim() !== '') { + return true + } + + return false + } catch (e) { + return false + } +} + +/** + * Get Gemini API key for API calls + * Reads from user profile settings (localStorage) or environment variables + */ +export function getGeminiConfig(): { apiKey: string } | null { + try { + // First try to get user-specific API keys if available + const session = JSON.parse(localStorage.getItem('session') || '{}') + if (session.authed && session.username) { + const userApiKeys = localStorage.getItem(`${session.username}_api_keys`) + if (userApiKeys) { + try { + const parsed = JSON.parse(userApiKeys) + if (parsed.keys && parsed.keys.gemini && parsed.keys.gemini.trim() !== '') { + console.log('🔑 Found user-specific Gemini API key') + return { apiKey: parsed.keys.gemini } + } + } catch (e) { + console.log('🔑 Error parsing user-specific API keys:', e) + } + } + } + + // Fallback to global API keys in localStorage + const settings = localStorage.getItem("gemini_api_key") + if (settings && settings.trim() !== '') { + console.log('🔑 Found global Gemini API key in localStorage') + return { apiKey: settings } + } + + // Fallback to environment variable + const config = getClientConfig() + if (config.geminiApiKey && config.geminiApiKey.trim() !== '') { + console.log('🔑 Found Gemini API key in environment') + return { apiKey: config.geminiApiKey } + } + + console.log('🔑 No Gemini API key found') + return null + } catch (e) { + console.log('🔑 Error getting Gemini config:', e) + return null + } +} diff --git a/src/shapes/MycroZineGeneratorShapeUtil.tsx b/src/shapes/MycroZineGeneratorShapeUtil.tsx index 121dff5..8c140b7 100644 --- a/src/shapes/MycroZineGeneratorShapeUtil.tsx +++ b/src/shapes/MycroZineGeneratorShapeUtil.tsx @@ -11,6 +11,7 @@ import React, { useState, useRef, useEffect } from "react" import { StandardizedToolWrapper } from "@/components/StandardizedToolWrapper" import { usePinnedToView } from "@/hooks/usePinnedToView" import { useMaximize } from "@/hooks/useMaximize" +import { getGeminiConfig } from "@/lib/clientConfig" // ============================================================================ // Types @@ -1059,15 +1060,154 @@ High contrast black and white with neon green accent highlights. Xerox texture, } async function generatePageImage(prompt: string, pageNumber: number): Promise { - // In actual implementation, this would call the Gemini MCP server - // For now, return a placeholder - console.log(`Generating page ${pageNumber} with prompt:`, prompt.substring(0, 100) + '...') + console.log(`🍄 Generating page ${pageNumber} with Gemini Nano Banana Pro...`) + console.log(`📝 Prompt preview:`, prompt.substring(0, 100) + '...') - // Simulate generation delay - await new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 1000)) + const geminiConfig = getGeminiConfig() - // Return placeholder image - return `https://via.placeholder.com/825x1275/1a1a1a/00ff00?text=Page+${pageNumber}` + if (!geminiConfig) { + console.warn('⚠️ No Gemini API key configured, using placeholder') + return `https://via.placeholder.com/825x1275/1a1a1a/00ff00?text=Page+${pageNumber}+%28No+API+Key%29` + } + + const { apiKey } = geminiConfig + + // Try Nano Banana Pro first (gemini-2.0-flash-exp-image-generation) + try { + const imageUrl = await generateWithNanoBananaPro(prompt, apiKey) + if (imageUrl) { + console.log(`✅ Page ${pageNumber} generated with Nano Banana Pro`) + return imageUrl + } + } catch (error) { + console.error(`❌ Nano Banana Pro failed for page ${pageNumber}:`, error) + } + + // Fallback to Gemini 2.0 Flash experimental + try { + const imageUrl = await generateWithGemini2Flash(prompt, apiKey) + if (imageUrl) { + console.log(`✅ Page ${pageNumber} generated with Gemini 2.0 Flash`) + return imageUrl + } + } catch (error) { + console.error(`❌ Gemini 2.0 Flash failed for page ${pageNumber}:`, error) + } + + // Final fallback: placeholder + console.warn(`⚠️ All generation methods failed for page ${pageNumber}, using placeholder`) + return `https://via.placeholder.com/825x1275/1a1a1a/00ff00?text=Page+${pageNumber}+%28Generation+Failed%29` +} + +// Types for Gemini API response +interface GeminiPart { + text?: string + inlineData?: { + mimeType: string + data: string + } +} + +interface GeminiCandidate { + content?: { + parts?: GeminiPart[] + } +} + +interface GeminiResponse { + candidates?: GeminiCandidate[] +} + +// Nano Banana Pro - highest quality, excellent text rendering +async function generateWithNanoBananaPro(prompt: string, apiKey: string): Promise { + const response = await fetch( + `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp-image-generation:generateContent?key=${apiKey}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + contents: [ + { + parts: [ + { + text: prompt, + }, + ], + }, + ], + generationConfig: { + responseModalities: ['IMAGE'], + }, + }), + } + ) + + if (!response.ok) { + const errorText = await response.text() + console.error('Nano Banana Pro API error:', response.status, errorText) + return null + } + + const data: GeminiResponse = await response.json() + + // Extract image from response + const parts = data.candidates?.[0]?.content?.parts || [] + for (const part of parts) { + if (part.inlineData?.mimeType?.startsWith('image/')) { + // Convert base64 to data URL + return `data:${part.inlineData.mimeType};base64,${part.inlineData.data}` + } + } + + return null +} + +// Gemini 2.0 Flash experimental fallback +async function generateWithGemini2Flash(prompt: string, apiKey: string): Promise { + const response = await fetch( + `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=${apiKey}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + contents: [ + { + parts: [ + { + text: `Generate an image: ${prompt}`, + }, + ], + }, + ], + generationConfig: { + responseModalities: ['IMAGE', 'TEXT'], + responseMimeType: 'image/png', + }, + }), + } + ) + + if (!response.ok) { + const errorText = await response.text() + console.error('Gemini 2.0 Flash error:', response.status, errorText) + return null + } + + const data: GeminiResponse = await response.json() + + // Extract image from response + const parts = data.candidates?.[0]?.content?.parts || [] + for (const part of parts) { + if (part.inlineData?.mimeType?.startsWith('image/')) { + return `data:${part.inlineData.mimeType};base64,${part.inlineData.data}` + } + } + + return null } async function spawnPageOnCanvas( diff --git a/worker/worker.ts b/worker/worker.ts index e840f37..e93a692 100644 --- a/worker/worker.ts +++ b/worker/worker.ts @@ -106,9 +106,10 @@ const { preflight, corsify } = cors({ } // For development - check if it's a localhost or local IP (both http and https) + // Includes: localhost, 127.x, 192.168.x, 169.254.x, 10.x, 172.16-31.x (private), 100.x (Tailscale) if ( origin.match( - /^https?:\/\/(localhost|127\.0\.0\.1|192\.168\.|169\.254\.|10\.)/, + /^https?:\/\/(localhost|127\.\d+\.\d+\.\d+|192\.168\.|169\.254\.|10\.|172\.(1[6-9]|2\d|3[01])\.|100\.)/, ) ) { return origin