diff --git a/backend/api/onboarding.py b/backend/api/onboarding.py index a94c59a3..8a901548 100644 --- a/backend/api/onboarding.py +++ b/backend/api/onboarding.py @@ -684,4 +684,77 @@ async def get_user_writing_personas(user_id: int = 1): return await get_user_personas(user_id) except Exception as e: logger.error(f"Error getting user personas: {str(e)}") - raise HTTPException(status_code=500, detail="Internal server error") \ No newline at end of file + raise HTTPException(status_code=500, detail="Internal server error") + +# Business Information endpoints +async def save_business_info(business_info: 'BusinessInfoRequest'): + """Save business information for users without websites.""" + try: + from models.business_info_request import BusinessInfoRequest + from services.business_info_service import business_info_service + + logger.info(f"🔄 Saving business info for user_id: {business_info.user_id}") + result = business_info_service.save_business_info(business_info) + logger.success(f"✅ Business info saved successfully for user_id: {business_info.user_id}") + return result + except Exception as e: + logger.error(f"❌ Error saving business info: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to save business info: {str(e)}") + +async def get_business_info(business_info_id: int): + """Get business information by ID.""" + try: + from services.business_info_service import business_info_service + + logger.info(f"🔄 Getting business info for ID: {business_info_id}") + result = business_info_service.get_business_info(business_info_id) + if result: + logger.success(f"✅ Business info retrieved for ID: {business_info_id}") + return result + else: + logger.warning(f"⚠️ No business info found for ID: {business_info_id}") + raise HTTPException(status_code=404, detail="Business info not found") + except HTTPException: + raise + except Exception as e: + logger.error(f"❌ Error getting business info: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to get business info: {str(e)}") + +async def get_business_info_by_user(user_id: int): + """Get business information by user ID.""" + try: + from services.business_info_service import business_info_service + + logger.info(f"🔄 Getting business info for user ID: {user_id}") + result = business_info_service.get_business_info_by_user(user_id) + if result: + logger.success(f"✅ Business info retrieved for user ID: {user_id}") + return result + else: + logger.warning(f"⚠️ No business info found for user ID: {user_id}") + raise HTTPException(status_code=404, detail="Business info not found") + except HTTPException: + raise + except Exception as e: + logger.error(f"❌ Error getting business info: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to get business info: {str(e)}") + +async def update_business_info(business_info_id: int, business_info: 'BusinessInfoRequest'): + """Update business information.""" + try: + from models.business_info_request import BusinessInfoRequest + from services.business_info_service import business_info_service + + logger.info(f"🔄 Updating business info for ID: {business_info_id}") + result = business_info_service.update_business_info(business_info_id, business_info) + if result: + logger.success(f"✅ Business info updated for ID: {business_info_id}") + return result + else: + logger.warning(f"⚠️ No business info found to update for ID: {business_info_id}") + raise HTTPException(status_code=404, detail="Business info not found") + except HTTPException: + raise + except Exception as e: + logger.error(f"❌ Error updating business info: {str(e)}") + raise HTTPException(status_code=500, detail=f"Failed to update business info: {str(e)}") diff --git a/backend/app.py b/backend/app.py index 1e06caed..19da115e 100644 --- a/backend/app.py +++ b/backend/app.py @@ -42,6 +42,10 @@ from api.onboarding import ( get_onboarding_summary, get_website_analysis_data, get_research_preferences_data, + save_business_info, + get_business_info, + get_business_info_by_user, + update_business_info, StepCompletionRequest, APIKeyRequest ) @@ -433,6 +437,45 @@ async def research_preferences_data(): logger.error(f"Error in research_preferences_data: {e}") raise HTTPException(status_code=500, detail=str(e)) +# Business Information endpoints +@app.post("/api/onboarding/business-info") +async def business_info_save(request: 'BusinessInfoRequest'): + """Save business information for users without websites.""" + try: + from models.business_info_request import BusinessInfoRequest + return await save_business_info(request) + except Exception as e: + logger.error(f"Error in business_info_save: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/api/onboarding/business-info/{business_info_id}") +async def business_info_get(business_info_id: int): + """Get business information by ID.""" + try: + return await get_business_info(business_info_id) + except Exception as e: + logger.error(f"Error in business_info_get: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +@app.get("/api/onboarding/business-info/user/{user_id}") +async def business_info_get_by_user(user_id: int): + """Get business information by user ID.""" + try: + return await get_business_info_by_user(user_id) + except Exception as e: + logger.error(f"Error in business_info_get_by_user: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +@app.put("/api/onboarding/business-info/{business_info_id}") +async def business_info_update(business_info_id: int, request: 'BusinessInfoRequest'): + """Update business information.""" + try: + from models.business_info_request import BusinessInfoRequest + return await update_business_info(business_info_id, request) + except Exception as e: + logger.error(f"Error in business_info_update: {e}") + raise HTTPException(status_code=500, detail=str(e)) + # Include component logic router app.include_router(component_logic_router) diff --git a/backend/models/business_info_request.py b/backend/models/business_info_request.py new file mode 100644 index 00000000..206ba98c --- /dev/null +++ b/backend/models/business_info_request.py @@ -0,0 +1,24 @@ +"""Business Information Request Models for ALwrity backend.""" +from pydantic import BaseModel, Field +from typing import Optional +from datetime import datetime + +class BusinessInfoRequest(BaseModel): + user_id: Optional[int] = None + business_description: str = Field(..., min_length=10, max_length=1000, description="Description of the business") + industry: Optional[str] = Field(None, max_length=100, description="Industry sector") + target_audience: Optional[str] = Field(None, max_length=500, description="Target audience description") + business_goals: Optional[str] = Field(None, max_length=1000, description="Business goals and objectives") + +class BusinessInfoResponse(BaseModel): + id: int + user_id: Optional[int] + business_description: str + industry: Optional[str] + target_audience: Optional[str] + business_goals: Optional[str] + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True diff --git a/backend/models/user_business_info.py b/backend/models/user_business_info.py new file mode 100644 index 00000000..e22084d7 --- /dev/null +++ b/backend/models/user_business_info.py @@ -0,0 +1,38 @@ +"""User Business Information Model for ALwrity backend.""" +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import Column, Integer, String, Text, DateTime, func +from loguru import logger +from datetime import datetime + +Base = declarative_base() + +logger.info("🔄 Loading UserBusinessInfo model...") + +class UserBusinessInfo(Base): + __tablename__ = 'user_business_info' + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, index=True, nullable=True) + business_description = Column(Text, nullable=False) + industry = Column(String(100), nullable=True) + target_audience = Column(Text, nullable=True) + business_goals = Column(Text, nullable=True) + created_at = Column(DateTime, default=func.now()) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + + def __repr__(self): + return f"" + + def to_dict(self): + return { + "id": self.id, + "user_id": self.user_id, + "business_description": self.business_description, + "industry": self.industry, + "target_audience": self.target_audience, + "business_goals": self.business_goals, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + } + +logger.info("✅ UserBusinessInfo model loaded successfully!") diff --git a/backend/services/business_info_service.py b/backend/services/business_info_service.py new file mode 100644 index 00000000..c94b4b70 --- /dev/null +++ b/backend/services/business_info_service.py @@ -0,0 +1,84 @@ +"""Business Information Service for ALwrity backend.""" +from sqlalchemy.orm import Session +from models.user_business_info import UserBusinessInfo +from models.business_info_request import BusinessInfoRequest, BusinessInfoResponse +from services.database import get_db +from loguru import logger +from typing import Optional + +logger.info("🔄 Loading BusinessInfoService...") + +class BusinessInfoService: + def __init__(self): + logger.info("🆕 Initializing BusinessInfoService...") + + def save_business_info(self, business_info: BusinessInfoRequest) -> BusinessInfoResponse: + db: Session = next(get_db()) + logger.debug(f"Attempting to save business info for user_id: {business_info.user_id}") + + # Check if business info already exists for this user + existing_info = db.query(UserBusinessInfo).filter(UserBusinessInfo.user_id == business_info.user_id).first() + + if existing_info: + logger.info(f"Existing business info found for user_id {business_info.user_id}, updating it.") + existing_info.business_description = business_info.business_description + existing_info.industry = business_info.industry + existing_info.target_audience = business_info.target_audience + existing_info.business_goals = business_info.business_goals + db.commit() + db.refresh(existing_info) + logger.success(f"Updated business info for user_id {business_info.user_id}, ID: {existing_info.id}") + return BusinessInfoResponse(**existing_info.to_dict()) + else: + logger.info(f"No existing business info for user_id {business_info.user_id}, creating new entry.") + db_business_info = UserBusinessInfo( + user_id=business_info.user_id, + business_description=business_info.business_description, + industry=business_info.industry, + target_audience=business_info.target_audience, + business_goals=business_info.business_goals + ) + db.add(db_business_info) + db.commit() + db.refresh(db_business_info) + logger.success(f"Saved new business info for user_id {business_info.user_id}, ID: {db_business_info.id}") + return BusinessInfoResponse(**db_business_info.to_dict()) + + def get_business_info(self, business_info_id: int) -> Optional[BusinessInfoResponse]: + db: Session = next(get_db()) + logger.debug(f"Retrieving business info by ID: {business_info_id}") + business_info = db.query(UserBusinessInfo).filter(UserBusinessInfo.id == business_info_id).first() + if business_info: + logger.debug(f"Found business info for ID: {business_info_id}") + return BusinessInfoResponse(**business_info.to_dict()) + logger.warning(f"No business info found for ID: {business_info_id}") + return None + + def get_business_info_by_user(self, user_id: int) -> Optional[BusinessInfoResponse]: + db: Session = next(get_db()) + logger.debug(f"Retrieving business info by user ID: {user_id}") + business_info = db.query(UserBusinessInfo).filter(UserBusinessInfo.user_id == user_id).first() + if business_info: + logger.debug(f"Found business info for user ID: {user_id}") + return BusinessInfoResponse(**business_info.to_dict()) + logger.warning(f"No business info found for user ID: {user_id}") + return None + + def update_business_info(self, business_info_id: int, business_info: BusinessInfoRequest) -> Optional[BusinessInfoResponse]: + db: Session = next(get_db()) + logger.debug(f"Updating business info for ID: {business_info_id}") + db_business_info = db.query(UserBusinessInfo).filter(UserBusinessInfo.id == business_info_id).first() + if db_business_info: + db_business_info.business_description = business_info.business_description + db_business_info.industry = business_info.industry + db_business_info.target_audience = business_info.target_audience + db_business_info.business_goals = business_info.business_goals + db.commit() + db.refresh(db_business_info) + logger.success(f"Updated business info for ID: {business_info_id}") + return BusinessInfoResponse(**db_business_info.to_dict()) + logger.warning(f"No business info found to update for ID: {business_info_id}") + return None + +business_info_service = BusinessInfoService() +logger.info("✅ BusinessInfoService loaded successfully!") diff --git a/backend/services/database.py b/backend/services/database.py index bdac5855..69f7f355 100644 --- a/backend/services/database.py +++ b/backend/services/database.py @@ -19,6 +19,7 @@ from models.enhanced_strategy_models import Base as EnhancedStrategyBase from models.monitoring_models import Base as MonitoringBase from models.persona_models import Base as PersonaBase from models.subscription_models import Base as SubscriptionBase +from models.user_business_info import Base as UserBusinessInfoBase # Database configuration DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///./alwrity.db') @@ -72,7 +73,8 @@ def init_database(): MonitoringBase.metadata.create_all(bind=engine) PersonaBase.metadata.create_all(bind=engine) SubscriptionBase.metadata.create_all(bind=engine) - logger.info("Database initialized successfully with all models including subscription system") + UserBusinessInfoBase.metadata.create_all(bind=engine) + logger.info("Database initialized successfully with all models including subscription system and business info") except SQLAlchemyError as e: logger.error(f"Error initializing database: {str(e)}") raise diff --git a/frontend/src/api/businessInfo.ts b/frontend/src/api/businessInfo.ts new file mode 100644 index 00000000..e0f13dd2 --- /dev/null +++ b/frontend/src/api/businessInfo.ts @@ -0,0 +1,49 @@ +import { apiClient } from './client'; + +console.log('🔄 Loading Business Info API client...'); + +export interface BusinessInfo { + user_id?: number; + business_description: string; + industry?: string; + target_audience?: string; + business_goals?: string; +} + +export interface BusinessInfoResponse extends BusinessInfo { + id: number; + created_at: string; + updated_at: string; +} + +export const businessInfoApi = { + saveBusinessInfo: async (data: BusinessInfo): Promise => { + console.log('API: Saving business info', data); + const response = await apiClient.post('/onboarding/business-info', data); + console.log('API: Business info saved successfully', response.data); + return response.data; + }, + + getBusinessInfo: async (id: number): Promise => { + console.log(`API: Getting business info for ID: ${id}`); + const response = await apiClient.get(`/onboarding/business-info/${id}`); + console.log('API: Business info retrieved successfully', response.data); + return response.data; + }, + + getBusinessInfoByUserId: async (userId: number): Promise => { + console.log(`API: Getting business info for user ID: ${userId}`); + const response = await apiClient.get(`/onboarding/business-info/user/${userId}`); + console.log('API: Business info retrieved successfully by user ID', response.data); + return response.data; + }, + + updateBusinessInfo: async (id: number, data: BusinessInfo): Promise => { + console.log(`API: Updating business info for ID: ${id}`, data); + const response = await apiClient.put(`/onboarding/business-info/${id}`, data); + console.log('API: Business info updated successfully', response.data); + return response.data; + }, +}; + +console.log('✅ Business Info API client loaded successfully!'); diff --git a/frontend/src/components/OnboardingWizard/BusinessDescriptionStep.tsx b/frontend/src/components/OnboardingWizard/BusinessDescriptionStep.tsx new file mode 100644 index 00000000..b25bca56 --- /dev/null +++ b/frontend/src/components/OnboardingWizard/BusinessDescriptionStep.tsx @@ -0,0 +1,145 @@ +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'; + +interface BusinessDescriptionStepProps { + onBack: () => void; + onContinue: () => void; +} + +const BusinessDescriptionStep: React.FC = ({ onBack, onContinue }) => { + const [formData, setFormData] = useState({ + business_description: '', + industry: '', + target_audience: '', + business_goals: '', + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ ...prev, [name]: value })); + }; + + const handleSaveAndContinue = async () => { + setError(null); + setSuccess(null); + setLoading(true); + console.log('🚀 Attempting to save business info:', formData); + + try { + // Simulate user_id for now, replace with actual user_id from auth context later + const userId = 1; + const dataToSave = { ...formData, user_id: userId }; + + const response = await businessInfoApi.saveBusinessInfo(dataToSave); + console.log('✅ Business info saved to DB:', response); + setSuccess('Business information saved successfully!'); + + setTimeout(() => { + onContinue(); + }, 1500); // Give user time to see success message + } catch (err) { + console.error('❌ Error saving business info:', err); + setError('Failed to save business information. Please try again.'); + } finally { + setLoading(false); + } + }; + + return ( + + + Tell us about your business + + + Since you don't have a website, please provide a description of your business. This will help ALwrity understand your brand and tailor its services. + + + + + {error && {error}} + {success && }>{success}} + + + + + + + + + + + + + + ); +}; + +export default BusinessDescriptionStep; diff --git a/frontend/src/components/OnboardingWizard/WebsiteStep.tsx b/frontend/src/components/OnboardingWizard/WebsiteStep.tsx index d0d0fc3d..8050e93b 100644 --- a/frontend/src/components/OnboardingWizard/WebsiteStep.tsx +++ b/frontend/src/components/OnboardingWizard/WebsiteStep.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import BusinessDescriptionStep from './BusinessDescriptionStep'; import { Box, Button, @@ -178,6 +179,7 @@ const WebsiteStep: React.FC = ({ onContinue, updateHeaderConte const [useAnalysisForGenAI, setUseAnalysisForGenAI] = useState(true); const [domainName, setDomainName] = useState(''); const [hasCheckedExisting, setHasCheckedExisting] = useState(false); + const [showBusinessForm, setShowBusinessForm] = useState(false); const [progress, setProgress] = useState([ { step: 1, message: 'Validating website URL', completed: false }, { step: 2, message: 'Crawling website content', completed: false }, @@ -926,6 +928,22 @@ const WebsiteStep: React.FC = ({ onContinue, updateHeaderConte ); + // Conditional rendering for business description form + if (showBusinessForm) { + return ( + { + console.log('⬅️ Going back to website form...'); + setShowBusinessForm(false); + }} + onContinue={() => { + console.log('➡️ Business info completed, proceeding to next step...'); + onContinue(); + }} + /> + ); + } + return ( {/* Enhanced Explanatory Text */} @@ -978,6 +996,22 @@ const WebsiteStep: React.FC = ({ onContinue, updateHeaderConte + {/* No Website Button */} + + + + {loading && ( diff --git a/tatus b/tatus new file mode 100644 index 00000000..39e1880f --- /dev/null +++ b/tatus @@ -0,0 +1,5 @@ +cc8f9cd2e (HEAD -> main, origin/cleanup/remove-cache-files, cleanup/remove-cache-files) Clean up: Remove all cache files and add comprehensive .gitignore +c19fc3f22 (origin/main, origin/HEAD) ALwrity Prompts - AI Integration Plan +5efee4235 Added citation and quality metrics to the content editor. +10b50f973 Alwrity Copilot Integration for LinkedIn Writer +64944104a merge: LinkedIn Writer PR #223 - resolve conflicts and integrate with existing routers