import { supabase } from '../db/client.js';
import type { DesignRecommendation } from '../types.js';
/**
* Recommend design combinations based on business type
*/
export async function recommendDesign(businessType: string): Promise<{
recommendations: DesignRecommendation[];
total: number;
}> {
try {
if (!businessType) {
throw new Error('Business type is required');
}
const client = supabase.getAnonClient();
// Get styles that match the business type
const { data: matchingStyles, error: stylesError } = await client
.from('design_styles')
.select('id, name, category')
.contains('businesses', [businessType]);
if (stylesError) {
supabase.log(`Error fetching matching styles: ${stylesError.message}`, 'error');
throw new Error(`Failed to fetch matching styles: ${stylesError.message}`);
}
if (!matchingStyles || matchingStyles.length === 0) {
// If no exact match, fall back to industry-based recommendations
return await recommendByIndustry(businessType);
}
const styleIds = matchingStyles.map(s => s.id);
// Get top palette combinations for these styles
const { data: compatibilityData, error: compatibilityError } = await client
.from('style_palette_compatibility')
.select(`
style_id,
palette_id,
score,
design_styles!inner(id, name, category),
design_palettes!inner(id, name, category)
`)
.in('style_id', styleIds)
.gte('score', 4) // Only high-scoring combinations
.order('score', { ascending: false })
.limit(20); // Get more than 5 to allow for filtering
if (compatibilityError) {
supabase.log(`Error fetching compatibility data: ${compatibilityError.message}`, 'error');
throw new Error(`Failed to fetch compatibility data: ${compatibilityError.message}`);
}
if (!compatibilityData || compatibilityData.length === 0) {
throw new Error(`No design recommendations found for business type: ${businessType}`);
}
// Convert to recommendation format and deduplicate
const recommendationMap = new Map<string, DesignRecommendation>();
for (const item of compatibilityData) {
const key = `${item.style_id}-${item.palette_id}`;
if (!recommendationMap.has(key)) {
const styleName = (item as any).design_styles?.name || 'Unknown Style';
const paletteName = (item as any).design_palettes?.name || 'Unknown Palette';
recommendationMap.set(key, {
styleId: item.style_id,
styleName,
paletteId: item.palette_id,
paletteName,
score: item.score,
reason: generateRecommendationReason(businessType, styleName, paletteName, item.score)
});
}
}
// Get top 5 unique combinations
const recommendations = Array.from(recommendationMap.values())
.sort((a, b) => b.score - a.score)
.slice(0, 5);
supabase.log(`Generated ${recommendations.length} recommendations for ${businessType}`);
return {
recommendations,
total: recommendations.length
};
} catch (error) {
supabase.log(`Error in recommendDesign: ${error}`, 'error');
throw error;
}
}
/**
* Fallback recommendation based on industry matching
*/
async function recommendByIndustry(businessType: string): Promise<{
recommendations: DesignRecommendation[];
total: number;
}> {
try {
const client = supabase.getAnonClient();
// Try to match by industry patterns
const industryMapping: Record<string, string[]> = {
'restaurant': ['SaaS', 'E-commerce'],
'cafe': ['SaaS', 'E-commerce'],
'shop': ['E-commerce', 'SaaS'],
'store': ['E-commerce', 'SaaS'],
'clinic': ['SaaS', 'Fintech'],
'dental': ['SaaS', 'Fintech'],
'law': ['SaaS', 'Fintech'],
'consulting': ['Fintech', 'SaaS'],
'fitness': ['SaaS', 'E-commerce'],
'gym': ['SaaS', 'E-commerce'],
'salon': ['SaaS', 'E-commerce'],
'spa': ['SaaS', 'E-commerce']
};
const businessLower = businessType.toLowerCase();
let targetIndustries: string[] = [];
for (const [key, industries] of Object.entries(industryMapping)) {
if (businessLower.includes(key)) {
targetIndustries = industries;
break;
}
}
if (targetIndustries.length === 0) {
// Default to SaaS styles as they're most versatile
targetIndustries = ['SaaS'];
}
// Get styles from these industries
const { data: industryStyles, error: stylesError } = await client
.from('design_styles')
.select('id, name, category')
.in('industry', targetIndustries)
.limit(10);
if (stylesError || !industryStyles || industryStyles.length === 0) {
throw new Error(`No suitable styles found for business type: ${businessType}`);
}
const styleIds = industryStyles.map(s => s.id);
// Get top combinations
const { data: compatibilityData, error: compatibilityError } = await client
.from('style_palette_compatibility')
.select(`
style_id,
palette_id,
score,
design_styles!inner(id, name, category),
design_palettes!inner(id, name, category)
`)
.in('style_id', styleIds)
.gte('score', 4)
.order('score', { ascending: false })
.limit(15);
if (compatibilityError || !compatibilityData || compatibilityData.length === 0) {
throw new Error(`No design recommendations found for business type: ${businessType}`);
}
// Convert to recommendations
const recommendationMap = new Map<string, DesignRecommendation>();
for (const item of compatibilityData) {
const key = `${item.style_id}-${item.palette_id}`;
if (!recommendationMap.has(key)) {
const styleName = (item as any).design_styles?.name || 'Unknown Style';
const paletteName = (item as any).design_palettes?.name || 'Unknown Palette';
recommendationMap.set(key, {
styleId: item.style_id,
styleName,
paletteId: item.palette_id,
paletteName,
score: item.score,
reason: generateRecommendationReason(businessType, styleName, paletteName, item.score, true)
});
}
}
const recommendations = Array.from(recommendationMap.values())
.sort((a, b) => b.score - a.score)
.slice(0, 5);
supabase.log(`Generated ${recommendations.length} fallback recommendations for ${businessType}`);
return {
recommendations,
total: recommendations.length
};
} catch (error) {
supabase.log(`Error in recommendByIndustry: ${error}`, 'error');
throw error;
}
}
/**
* Generate a human-readable reason for the recommendation
*/
function generateRecommendationReason(
businessType: string,
styleName: string,
paletteName: string,
score: number,
isFallback: boolean = false
): string {
const scoreText = score === 5 ? 'Perfect match' : score === 4 ? 'Great match' : 'Good match';
const fallbackText = isFallback ? ' (based on similar industries)' : '';
const reasons = [
`${scoreText} for ${businessType} businesses${fallbackText}. ${styleName} provides the right design approach while ${paletteName} creates the appropriate visual mood.`,
`${scoreText} - ${styleName} style works excellently with ${paletteName} colors for ${businessType} businesses${fallbackText}.`,
`${scoreText} combination. ${styleName} offers the perfect design foundation and ${paletteName} delivers ideal branding colors for ${businessType}${fallbackText}.`
];
// Return a random reason to add variety
return reasons[Math.floor(Math.random() * reasons.length)];
}