/** * Client-side configuration utility * Handles environment variables in browser environment */ export interface ClientConfig { githubToken?: string quartzRepo?: string quartzBranch?: string cloudflareApiKey?: string cloudflareAccountId?: string quartzApiUrl?: string quartzApiKey?: string webhookUrl?: string webhookSecret?: string openaiApiKey?: string runpodApiKey?: string runpodEndpointId?: string runpodImageEndpointId?: string runpodVideoEndpointId?: string runpodTextEndpointId?: string runpodWhisperEndpointId?: string ollamaUrl?: string } /** * Get client-side configuration * This works in both browser and server environments */ export function getClientConfig(): ClientConfig { // In Vite, environment variables are available via import.meta.env // In Next.js, NEXT_PUBLIC_ variables are available at build time if (typeof window !== 'undefined') { // Browser environment - check for Vite first, then Next.js if (typeof import.meta !== 'undefined' && import.meta.env) { // Vite environment return { githubToken: import.meta.env.VITE_GITHUB_TOKEN || import.meta.env.NEXT_PUBLIC_GITHUB_TOKEN, quartzRepo: import.meta.env.VITE_QUARTZ_REPO || import.meta.env.NEXT_PUBLIC_QUARTZ_REPO, quartzBranch: import.meta.env.VITE_QUARTZ_BRANCH || import.meta.env.NEXT_PUBLIC_QUARTZ_BRANCH, cloudflareApiKey: import.meta.env.VITE_CLOUDFLARE_API_KEY || import.meta.env.NEXT_PUBLIC_CLOUDFLARE_API_KEY, cloudflareAccountId: import.meta.env.VITE_CLOUDFLARE_ACCOUNT_ID || import.meta.env.NEXT_PUBLIC_CLOUDFLARE_ACCOUNT_ID, quartzApiUrl: import.meta.env.VITE_QUARTZ_API_URL || import.meta.env.NEXT_PUBLIC_QUARTZ_API_URL, quartzApiKey: import.meta.env.VITE_QUARTZ_API_KEY || import.meta.env.NEXT_PUBLIC_QUARTZ_API_KEY, webhookUrl: import.meta.env.VITE_QUARTZ_WEBHOOK_URL || import.meta.env.NEXT_PUBLIC_QUARTZ_WEBHOOK_URL, webhookSecret: import.meta.env.VITE_QUARTZ_WEBHOOK_SECRET || import.meta.env.NEXT_PUBLIC_QUARTZ_WEBHOOK_SECRET, openaiApiKey: import.meta.env.VITE_OPENAI_API_KEY || import.meta.env.NEXT_PUBLIC_OPENAI_API_KEY, runpodApiKey: import.meta.env.VITE_RUNPOD_API_KEY || import.meta.env.NEXT_PUBLIC_RUNPOD_API_KEY, runpodEndpointId: import.meta.env.VITE_RUNPOD_ENDPOINT_ID || import.meta.env.VITE_RUNPOD_IMAGE_ENDPOINT_ID || import.meta.env.NEXT_PUBLIC_RUNPOD_ENDPOINT_ID, runpodImageEndpointId: import.meta.env.VITE_RUNPOD_IMAGE_ENDPOINT_ID || import.meta.env.NEXT_PUBLIC_RUNPOD_IMAGE_ENDPOINT_ID, runpodVideoEndpointId: import.meta.env.VITE_RUNPOD_VIDEO_ENDPOINT_ID || import.meta.env.NEXT_PUBLIC_RUNPOD_VIDEO_ENDPOINT_ID, 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, } } else { // Next.js environment return { githubToken: (window as any).__NEXT_DATA__?.env?.NEXT_PUBLIC_GITHUB_TOKEN, quartzRepo: (window as any).__NEXT_DATA__?.env?.NEXT_PUBLIC_QUARTZ_REPO, quartzBranch: (window as any).__NEXT_DATA__?.env?.NEXT_PUBLIC_QUARTZ_BRANCH, cloudflareApiKey: (window as any).__NEXT_DATA__?.env?.NEXT_PUBLIC_CLOUDFLARE_API_KEY, cloudflareAccountId: (window as any).__NEXT_DATA__?.env?.NEXT_PUBLIC_CLOUDFLARE_ACCOUNT_ID, quartzApiUrl: (window as any).__NEXT_DATA__?.env?.NEXT_PUBLIC_QUARTZ_API_URL, quartzApiKey: (window as any).__NEXT_DATA__?.env?.NEXT_PUBLIC_QUARTZ_API_KEY, webhookUrl: (window as any).__NEXT_DATA__?.env?.NEXT_PUBLIC_QUARTZ_WEBHOOK_URL, webhookSecret: (window as any).__NEXT_DATA__?.env?.NEXT_PUBLIC_QUARTZ_WEBHOOK_SECRET, openaiApiKey: (window as any).__NEXT_DATA__?.env?.NEXT_PUBLIC_OPENAI_API_KEY, runpodApiKey: (window as any).__NEXT_DATA__?.env?.NEXT_PUBLIC_RUNPOD_API_KEY, runpodEndpointId: (window as any).__NEXT_DATA__?.env?.NEXT_PUBLIC_RUNPOD_ENDPOINT_ID, } } } else { // Server environment return { githubToken: process.env.VITE_GITHUB_TOKEN || process.env.NEXT_PUBLIC_GITHUB_TOKEN, quartzRepo: process.env.VITE_QUARTZ_REPO || process.env.NEXT_PUBLIC_QUARTZ_REPO, quartzBranch: process.env.VITE_QUARTZ_BRANCH || process.env.NEXT_PUBLIC_QUARTZ_BRANCH, cloudflareApiKey: process.env.VITE_CLOUDFLARE_API_KEY || process.env.NEXT_PUBLIC_CLOUDFLARE_API_KEY, cloudflareAccountId: process.env.VITE_CLOUDFLARE_ACCOUNT_ID || process.env.NEXT_PUBLIC_CLOUDFLARE_ACCOUNT_ID, quartzApiUrl: process.env.VITE_QUARTZ_API_URL || process.env.NEXT_PUBLIC_QUARTZ_API_URL, quartzApiKey: process.env.VITE_QUARTZ_API_KEY || process.env.NEXT_PUBLIC_QUARTZ_API_KEY, webhookUrl: process.env.VITE_QUARTZ_WEBHOOK_URL || process.env.NEXT_PUBLIC_QUARTZ_WEBHOOK_URL, webhookSecret: process.env.VITE_QUARTZ_WEBHOOK_SECRET || process.env.NEXT_PUBLIC_QUARTZ_WEBHOOK_SECRET, runpodApiKey: process.env.VITE_RUNPOD_API_KEY || process.env.NEXT_PUBLIC_RUNPOD_API_KEY, runpodEndpointId: process.env.VITE_RUNPOD_ENDPOINT_ID || process.env.VITE_RUNPOD_IMAGE_ENDPOINT_ID || process.env.NEXT_PUBLIC_RUNPOD_ENDPOINT_ID, runpodImageEndpointId: process.env.VITE_RUNPOD_IMAGE_ENDPOINT_ID || process.env.NEXT_PUBLIC_RUNPOD_IMAGE_ENDPOINT_ID, runpodVideoEndpointId: process.env.VITE_RUNPOD_VIDEO_ENDPOINT_ID || process.env.NEXT_PUBLIC_RUNPOD_VIDEO_ENDPOINT_ID, 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, } } } // Default RunPod API key - shared across all endpoints // This allows all users to access AI features without their own API keys const DEFAULT_RUNPOD_API_KEY = 'rpa_YYOARL5MEBTTKKWGABRKTW2CVHQYRBTOBZNSGIL3lwwfdz' // Default RunPod endpoint IDs (from CLAUDE.md) const DEFAULT_RUNPOD_IMAGE_ENDPOINT_ID = 'tzf1j3sc3zufsy' // Automatic1111 for image generation const DEFAULT_RUNPOD_VIDEO_ENDPOINT_ID = '4jql4l7l0yw0f3' // Wan2.2 for video generation const DEFAULT_RUNPOD_TEXT_ENDPOINT_ID = '03g5hz3hlo8gr2' // vLLM for text generation const DEFAULT_RUNPOD_WHISPER_ENDPOINT_ID = 'lrtisuv8ixbtub' // Whisper for transcription /** * Get RunPod configuration for API calls (defaults to image endpoint) * Falls back to pre-configured endpoints if not set via environment */ export function getRunPodConfig(): { apiKey: string; endpointId: string } | null { const config = getClientConfig() const apiKey = config.runpodApiKey || DEFAULT_RUNPOD_API_KEY const endpointId = config.runpodEndpointId || config.runpodImageEndpointId || DEFAULT_RUNPOD_IMAGE_ENDPOINT_ID return { apiKey: apiKey, endpointId: endpointId } } /** * Get RunPod configuration for image generation * Falls back to pre-configured Automatic1111 endpoint */ export function getRunPodImageConfig(): { apiKey: string; endpointId: string } | null { const config = getClientConfig() const apiKey = config.runpodApiKey || DEFAULT_RUNPOD_API_KEY const endpointId = config.runpodImageEndpointId || config.runpodEndpointId || DEFAULT_RUNPOD_IMAGE_ENDPOINT_ID return { apiKey: apiKey, endpointId: endpointId } } /** * Get RunPod configuration for video generation * Falls back to pre-configured Wan2.2 endpoint */ export function getRunPodVideoConfig(): { apiKey: string; endpointId: string } | null { const config = getClientConfig() const apiKey = config.runpodApiKey || DEFAULT_RUNPOD_API_KEY const endpointId = config.runpodVideoEndpointId || DEFAULT_RUNPOD_VIDEO_ENDPOINT_ID return { apiKey: apiKey, endpointId: endpointId } } /** * Get RunPod configuration for text generation (vLLM) * Falls back to pre-configured vLLM endpoint */ export function getRunPodTextConfig(): { apiKey: string; endpointId: string } | null { const config = getClientConfig() const apiKey = config.runpodApiKey || DEFAULT_RUNPOD_API_KEY const endpointId = config.runpodTextEndpointId || DEFAULT_RUNPOD_TEXT_ENDPOINT_ID return { apiKey: apiKey, endpointId: endpointId } } /** * Get RunPod configuration for Whisper transcription * Falls back to pre-configured Whisper endpoint */ export function getRunPodWhisperConfig(): { apiKey: string; endpointId: string } | null { const config = getClientConfig() const apiKey = config.runpodApiKey || DEFAULT_RUNPOD_API_KEY const endpointId = config.runpodWhisperEndpointId || DEFAULT_RUNPOD_WHISPER_ENDPOINT_ID return { apiKey: apiKey, endpointId: endpointId } } /** * Get Ollama configuration for local LLM * Falls back to the default Netcup AI Orchestrator if not configured */ export function getOllamaConfig(): { url: string } | null { const config = getClientConfig() // Default to Netcup AI Orchestrator (Ollama) if not configured // This ensures all users have free AI access without needing their own API keys const ollamaUrl = config.ollamaUrl || 'https://ai.jeffemmett.com' return { url: ollamaUrl } } /** * Check if RunPod integration is configured */ export function isRunPodConfigured(): boolean { const config = getClientConfig() return !!(config.runpodApiKey && config.runpodEndpointId) } /** * Check if GitHub integration is configured */ export function isGitHubConfigured(): boolean { const config = getClientConfig() return !!(config.githubToken && config.quartzRepo) } /** * Get GitHub configuration for API calls */ export function getGitHubConfig(): { token: string; repo: string; branch: string } | null { const config = getClientConfig() if (!config.githubToken || !config.quartzRepo) { return null } const [owner, repo] = config.quartzRepo.split('/') if (!owner || !repo) { return null } return { token: config.githubToken, repo: config.quartzRepo, branch: config.quartzBranch || 'main' } } /** * Check if OpenAI integration is configured * Reads from user profile settings (localStorage) instead of environment variables */ export function isOpenAIConfigured(): 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.openai && parsed.keys.openai.trim() !== '') { return true } } catch (e) { // Continue to fallback } } } // Fallback to global API keys const settings = localStorage.getItem("openai_api_key") if (settings) { try { const parsed = JSON.parse(settings) if (parsed.keys && parsed.keys.openai && parsed.keys.openai.trim() !== '') { return true } } catch (e) { // If it's not JSON, it might be the old format (just a string) if (settings.startsWith('sk-') && settings.trim() !== '') { return true } } } return false } catch (e) { return false } } /** * Get OpenAI API key for API calls * Reads from user profile settings (localStorage) instead of environment variables */ export function getOpenAIConfig(): { 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.openai && parsed.keys.openai.trim() !== '') { console.log('🔑 Found user-specific OpenAI API key') return { apiKey: parsed.keys.openai } } } catch (e) { console.log('🔑 Error parsing user-specific API keys:', e) } } } // Fallback to global API keys const settings = localStorage.getItem("openai_api_key") if (settings) { try { const parsed = JSON.parse(settings) if (parsed.keys && parsed.keys.openai && parsed.keys.openai.trim() !== '') { console.log('🔑 Found global OpenAI API key') return { apiKey: parsed.keys.openai } } } catch (e) { // If it's not JSON, it might be the old format (just a string) if (settings.startsWith('sk-') && settings.trim() !== '') { console.log('🔑 Found old format OpenAI API key') return { apiKey: settings } } } } console.log('🔑 No OpenAI API key found') return null } catch (e) { console.log('🔑 Error getting OpenAI config:', e) return null } }