diff --git a/backend/database/migrations/add_business_info_table.sql b/backend/database/migrations/add_business_info_table.sql new file mode 100644 index 00000000..ed1b049e --- /dev/null +++ b/backend/database/migrations/add_business_info_table.sql @@ -0,0 +1,27 @@ +-- Migration: Add user_business_info table +-- Description: Creates table for storing business information when users don't have websites +-- Date: 2024-01-XX + +CREATE TABLE IF NOT EXISTS user_business_info ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + business_description TEXT NOT NULL, + industry VARCHAR(100), + target_audience TEXT, + business_goals TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Create index for faster user lookups +CREATE INDEX IF NOT EXISTS idx_user_business_info_user_id ON user_business_info(user_id); + +-- Add trigger to automatically update updated_at timestamp +CREATE TRIGGER IF NOT EXISTS update_user_business_info_timestamp + AFTER UPDATE ON user_business_info + FOR EACH ROW +BEGIN + UPDATE user_business_info + SET updated_at = CURRENT_TIMESTAMP + WHERE id = NEW.id; +END; diff --git a/backend/scripts/run_business_info_migration.py b/backend/scripts/run_business_info_migration.py new file mode 100644 index 00000000..ac886e73 --- /dev/null +++ b/backend/scripts/run_business_info_migration.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +Migration script to create the user_business_info table. +This script should be run once to set up the database schema. +""" + +import os +import sys +import sqlite3 +from pathlib import Path +from loguru import logger + +# Add the backend directory to the Python path +backend_dir = Path(__file__).parent.parent +sys.path.insert(0, str(backend_dir)) + +def run_migration(): + """Run the business info table migration.""" + try: + # Get the database path + db_path = backend_dir / "alwrity.db" + + logger.info(f"🔄 Starting business info table migration...") + logger.info(f"📁 Database path: {db_path}") + + # Check if database exists + if not db_path.exists(): + logger.warning(f"âš ī¸ Database file not found at {db_path}") + logger.info("📝 Creating new database file...") + + # Read the migration SQL + migration_file = backend_dir / "database" / "migrations" / "add_business_info_table.sql" + + if not migration_file.exists(): + logger.error(f"❌ Migration file not found: {migration_file}") + return False + + with open(migration_file, 'r') as f: + migration_sql = f.read() + + logger.info("📋 Migration SQL loaded successfully") + + # Connect to database and run migration + conn = sqlite3.connect(str(db_path)) + cursor = conn.cursor() + + # Check if table already exists + cursor.execute(""" + SELECT name FROM sqlite_master + WHERE type='table' AND name='user_business_info' + """) + + if cursor.fetchone(): + logger.info("â„šī¸ Table 'user_business_info' already exists, skipping migration") + conn.close() + return True + + # Execute the migration + cursor.executescript(migration_sql) + conn.commit() + + # Verify the table was created + cursor.execute(""" + SELECT name FROM sqlite_master + WHERE type='table' AND name='user_business_info' + """) + + if cursor.fetchone(): + logger.success("✅ Migration completed successfully!") + logger.info("📊 Table 'user_business_info' created with the following structure:") + + # Show table structure + cursor.execute("PRAGMA table_info(user_business_info)") + columns = cursor.fetchall() + + for col in columns: + logger.info(f" - {col[1]} ({col[2]}) {'NOT NULL' if col[3] else 'NULL'}") + + conn.close() + return True + else: + logger.error("❌ Migration failed - table was not created") + conn.close() + return False + + except Exception as e: + logger.error(f"❌ Migration failed with error: {str(e)}") + return False + +if __name__ == "__main__": + logger.info("🚀 Starting ALwrity Business Info Migration") + + success = run_migration() + + if success: + logger.success("🎉 Migration completed successfully!") + sys.exit(0) + else: + logger.error("đŸ’Ĩ Migration failed!") + sys.exit(1) diff --git a/backend/services/api_key_manager.py b/backend/services/api_key_manager.py index 1f6448dd..f8e33030 100644 --- a/backend/services/api_key_manager.py +++ b/backend/services/api_key_manager.py @@ -430,8 +430,11 @@ class APIKeyManager: def load_api_keys(self): """Load API keys from environment variables.""" - # Reload environment variables first - load_dotenv(override=True) + # Reload environment variables first - use backend directory path + import os + backend_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + env_path = os.path.join(backend_dir, ".env") + load_dotenv(env_path, override=True) env_mapping = { "OPENAI_API_KEY": "openai", @@ -492,8 +495,10 @@ class APIKeyManager: # Update environment variable os.environ[env_var] = api_key - # Update .env file - env_path = ".env" + # Update .env file - use backend directory path + import os + backend_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + env_path = os.path.join(backend_dir, ".env") if os.path.exists(env_path): with open(env_path, 'r') as f: lines = f.readlines() diff --git a/frontend/src/components/OnboardingWizard/BusinessDescriptionStep.tsx b/frontend/src/components/OnboardingWizard/BusinessDescriptionStep.tsx index b25bca56..ba5ec3cf 100644 --- a/frontend/src/components/OnboardingWizard/BusinessDescriptionStep.tsx +++ b/frontend/src/components/OnboardingWizard/BusinessDescriptionStep.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'; import { Box, Button, TextField, Typography, Card, CardContent, CircularProgress, Alert } from '@mui/material'; import { ArrowBack as ArrowBackIcon, Save as SaveIcon, CheckCircle as CheckCircleIcon } from '@mui/icons-material'; import { businessInfoApi, BusinessInfo } from '../../api/businessInfo'; +import { onboardingCache } from '../../services/onboardingCache'; interface BusinessDescriptionStepProps { onBack: () => void; @@ -19,6 +20,17 @@ const BusinessDescriptionStep: React.FC = ({ onBac const [error, setError] = useState(null); const [success, setSuccess] = useState(null); + useEffect(() => { + console.log('🔄 BusinessDescriptionStep mounted. Loading cached data...'); + const cachedData = onboardingCache.getStepData(2)?.businessInfo; + if (cachedData) { + setFormData(cachedData); + console.log('✅ Loaded cached business info:', cachedData); + } else { + console.log('â„šī¸ No cached business info found.'); + } + }, []); + const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); @@ -39,6 +51,10 @@ const BusinessDescriptionStep: React.FC = ({ onBac console.log('✅ Business info saved to DB:', response); setSuccess('Business information saved successfully!'); + // Also save to cache for consistency with other steps + onboardingCache.saveStepData(2, { businessInfo: response, hasWebsite: false }); + console.log('✅ Business info saved to cache.'); + setTimeout(() => { onContinue(); }, 1500); // Give user time to see success message diff --git a/frontend/src/services/onboardingCache.ts b/frontend/src/services/onboardingCache.ts new file mode 100644 index 00000000..a9931381 --- /dev/null +++ b/frontend/src/services/onboardingCache.ts @@ -0,0 +1,181 @@ +/** + * Onboarding Cache Service + * Manages client-side caching of onboarding data until final submission + */ + +interface OnboardingCacheData { + step1?: { + apiKeys?: Record; + providers?: string[]; + }; + step2?: { + website?: string; + analysis?: any; + businessInfo?: any; + hasWebsite?: boolean; + }; + step3?: { + researchPreferences?: any; + }; + step4?: { + personalization?: any; + }; + step5?: { + integrations?: any; + }; +} + +class OnboardingCacheService { + private readonly CACHE_KEY = 'alwrity_onboarding_cache'; + private readonly EXPIRY_KEY = 'alwrity_onboarding_cache_expiry'; + private readonly CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours + + /** + * Save data for a specific step + */ + saveStepData(stepNumber: number, data: any): void { + try { + const cache = this.getCache(); + const stepKey = `step${stepNumber}` as keyof OnboardingCacheData; + cache[stepKey] = { ...cache[stepKey], ...data }; + this.setCache(cache); + console.log(`✅ Onboarding cache: Saved step ${stepNumber} data`, data); + } catch (error) { + console.error(`❌ Onboarding cache: Failed to save step ${stepNumber} data`, error); + } + } + + /** + * Get data for a specific step + */ + getStepData(stepNumber: number): any { + try { + const cache = this.getCache(); + const stepKey = `step${stepNumber}` as keyof OnboardingCacheData; + const data = cache[stepKey]; + console.log(`📋 Onboarding cache: Retrieved step ${stepNumber} data`, data); + return data; + } catch (error) { + console.error(`❌ Onboarding cache: Failed to get step ${stepNumber} data`, error); + return null; + } + } + + /** + * Get all cached data + */ + getAllData(): OnboardingCacheData { + try { + const cache = this.getCache(); + console.log('📋 Onboarding cache: Retrieved all data', cache); + return cache; + } catch (error) { + console.error('❌ Onboarding cache: Failed to get all data', error); + return {}; + } + } + + /** + * Clear all cached data + */ + clearCache(): void { + try { + localStorage.removeItem(this.CACHE_KEY); + localStorage.removeItem(this.EXPIRY_KEY); + console.log('đŸ—‘ī¸ Onboarding cache: Cleared all data'); + } catch (error) { + console.error('❌ Onboarding cache: Failed to clear cache', error); + } + } + + /** + * Check if cache is valid (not expired) + */ + isCacheValid(): boolean { + try { + const expiry = localStorage.getItem(this.EXPIRY_KEY); + if (!expiry) return false; + + const expiryTime = parseInt(expiry, 10); + const now = Date.now(); + const isValid = now < expiryTime; + + if (!isValid) { + console.log('⏰ Onboarding cache: Cache expired, clearing...'); + this.clearCache(); + } + + return isValid; + } catch (error) { + console.error('❌ Onboarding cache: Failed to check cache validity', error); + return false; + } + } + + /** + * Get cache from localStorage + */ + private getCache(): OnboardingCacheData { + if (!this.isCacheValid()) { + return {}; + } + + try { + const cached = localStorage.getItem(this.CACHE_KEY); + return cached ? JSON.parse(cached) : {}; + } catch (error) { + console.error('❌ Onboarding cache: Failed to parse cache data', error); + return {}; + } + } + + /** + * Set cache in localStorage + */ + private setCache(data: OnboardingCacheData): void { + try { + const expiry = Date.now() + this.CACHE_DURATION; + localStorage.setItem(this.CACHE_KEY, JSON.stringify(data)); + localStorage.setItem(this.EXPIRY_KEY, expiry.toString()); + console.log('💾 Onboarding cache: Data saved to localStorage'); + } catch (error) { + console.error('❌ Onboarding cache: Failed to save to localStorage', error); + } + } + + /** + * Get API keys from cache + */ + getApiKeys(): Record { + const step1Data = this.getStepData(1); + return step1Data?.apiKeys || {}; + } + + /** + * Save API key to cache + */ + saveApiKey(provider: string, apiKey: string): void { + const step1Data = this.getStepData(1) || {}; + const apiKeys = step1Data.apiKeys || {}; + apiKeys[provider] = apiKey; + this.saveStepData(1, { ...step1Data, apiKeys }); + } + + /** + * Get website data from cache + */ + getWebsiteData(): any { + return this.getStepData(2); + } + + /** + * Save website data to cache + */ + saveWebsiteData(data: any): void { + this.saveStepData(2, data); + } +} + +// Export singleton instance +export const onboardingCache = new OnboardingCacheService(); +console.log('✅ Onboarding Cache Service loaded successfully!');