From 24c80559c8fa567e391a37595df163c54754bfd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yusuf=20=C4=B0pek?= Date: Sun, 23 Nov 2025 13:43:31 +0300 Subject: [PATCH] refactor: Remove experience level selection and simplify category display in onboarding modal. --- src/components/OnboardingModal.tsx | 86 +--- src/contexts/LocaleContext.tsx | 14 +- src/data/recommendationPresets.ts | 611 ++++++++++---------------- src/hooks/useRecommendationProfile.ts | 10 +- src/services/recommendationService.ts | 205 +-------- 5 files changed, 278 insertions(+), 648 deletions(-) diff --git a/src/components/OnboardingModal.tsx b/src/components/OnboardingModal.tsx index 01ca110..1617569 100644 --- a/src/components/OnboardingModal.tsx +++ b/src/components/OnboardingModal.tsx @@ -5,9 +5,8 @@ import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { X, ChevronRight, ChevronLeft, Sparkles } from 'lucide-react' -import { UserCategory, ExperienceLevel } from '@/types/recommendations' +import { UserCategory } from '@/types/recommendations' import { useLocale } from '@/contexts/LocaleContext' -import { RECOMMENDATION_PRESETS } from '@/data/recommendationPresets' import { CATEGORY_ICONS } from '@/constants/categoryIcons' interface OnboardingModalProps { @@ -16,7 +15,6 @@ interface OnboardingModalProps { onComplete: (data: { categories: UserCategory[] selectedOS?: string - experienceLevel: ExperienceLevel }) => void detectedOS: string } @@ -54,7 +52,6 @@ export function OnboardingModal({ const [selectedOS, setSelectedOS] = useState( detectedOS !== 'unknown' ? detectedOS : 'ubuntu' ) - const [experienceLevel, setExperienceLevel] = useState('beginner') // Reset state when modal opens useEffect(() => { @@ -72,16 +69,12 @@ export function OnboardingModal({ if (prev.includes(category)) { return prev.filter(c => c !== category) } - // Limit to 3 categories - if (prev.length >= 3) { - return [...prev.slice(1), category] - } return [...prev, category] }) } const handleNext = () => { - if (step < 3) { + if (step < 2) { setStep(step + 1) } } @@ -99,8 +92,7 @@ export function OnboardingModal({ onComplete({ categories: selectedCategories, - selectedOS: selectedOS, - experienceLevel + selectedOS: selectedOS }) // Close modal @@ -109,8 +101,7 @@ export function OnboardingModal({ const canProceed = () => { if (step === 1) return selectedCategories.length > 0 - if (step === 2) return selectedOS !== 'unknown' - if (step === 3) return true + if (step === 2) return true return false } @@ -136,7 +127,7 @@ export function OnboardingModal({ {/* Progress indicator */}
- {[1, 2, 3].map(i => ( + {[1, 2].map(i => (
- {RECOMMENDATION_PRESETS.map(preset => { - const Icon = CATEGORY_ICONS[preset.category] + {(Object.entries(CATEGORY_ICONS) as [UserCategory, any][]).map(([category, Icon]) => { return ( ) })} @@ -245,39 +231,7 @@ export function OnboardingModal({
)} - {/* Step 3: Experience Level */} - {step === 3 && ( -
-
-

- {t('onboarding.step3.title')} -

-

- {t('onboarding.step3.description')} -

-
-
- {(['beginner', 'intermediate', 'advanced'] as ExperienceLevel[]).map(level => ( - - ))} -
-
- )} {/* Navigation Buttons */}
@@ -292,7 +246,7 @@ export function OnboardingModal({ )} - {step < 3 ? ( + {step < 2 ? ( )}
diff --git a/src/contexts/LocaleContext.tsx b/src/contexts/LocaleContext.tsx index 3233454..481272c 100644 --- a/src/contexts/LocaleContext.tsx +++ b/src/contexts/LocaleContext.tsx @@ -14,7 +14,8 @@ const translations = { next: "Next", back: "Back", select_all: "Select All", - deselect_all: "Deselect All" + deselect_all: "Deselect All", + finish: "Finish" }, platform: { select: "Select Your Platform", @@ -117,9 +118,7 @@ const translations = { complete: "Get My Recommendations", step1: { title: "What do you want to use your computer for?", - description: "Select up to 3 categories that match your needs", - selected: "{count} selected (max 3)", - all_selected: "All {count} categories selected" + description: "Select the categories that match your needs" }, step2: { title: "Select your operating system", @@ -205,7 +204,8 @@ const translations = { next: "İleri", back: "Geri", select_all: "Tümünü Seç", - deselect_all: "Seçimi Kaldır" + deselect_all: "Seçimi Kaldır", + finish: "Bitir" }, platform: { select: "Platformunuzu Seçin", @@ -308,9 +308,7 @@ const translations = { complete: "Önerilerimi Getir", step1: { title: "Bilgisayarınızı ne için kullanmak istiyorsunuz?", - description: "İhtiyaçlarınıza uygun en fazla 3 kategori seçin", - selected: "{count} seçildi (max 3)", - all_selected: "Tüm {count} kategori seçildi" + description: "İhtiyaçlarınıza uygun kategorileri seçin" }, step2: { title: "İşletim sisteminizi seçin", diff --git a/src/data/recommendationPresets.ts b/src/data/recommendationPresets.ts index 0e82e1c..e4b9ee5 100644 --- a/src/data/recommendationPresets.ts +++ b/src/data/recommendationPresets.ts @@ -1,403 +1,254 @@ -import { CategoryPreset } from "@/types/recommendations"; +import { UserCategory } from "@/types/recommendations"; /** - * Curated package recommendations for each user category - * These presets are used by the recommendation engine to suggest packages + * Curated package recommendations for each platform and category + * Simple structure: Platform → Category → Package names + * + * Edit this file to add/remove packages for each platform/category combination */ -export const RECOMMENDATION_PRESETS: CategoryPreset[] = [ - { - category: "development", - description: "Essential tools for software development", - packages: [ - { - packageName: "git", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 10, - reason: "Version control system essential for all developers", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "code", - platforms: ["windows", "macos", "ubuntu", "debian"], - priority: 9, - reason: "Visual Studio Code - Popular code editor", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "visual-studio-code", - platforms: ["arch", "fedora"], - priority: 9, - reason: "Visual Studio Code - Popular code editor", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "nodejs", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 8, - reason: "JavaScript runtime for modern web development", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "python3", - platforms: ["ubuntu", "debian", "arch", "fedora"], - priority: 8, - reason: "Python programming language", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "python", - platforms: ["windows", "macos"], - priority: 8, - reason: "Python programming language", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "docker", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 7, - reason: "Containerization platform for development", - experienceLevel: ["intermediate", "advanced"], - }, - { - packageName: "curl", - platforms: ["ubuntu", "debian", "arch", "fedora", "macos"], - priority: 7, - reason: "Command-line tool for transferring data", - experienceLevel: ["intermediate", "advanced"], - }, - { - packageName: "vim", - platforms: ["ubuntu", "debian", "arch", "fedora", "macos"], - priority: 6, - reason: "Powerful text editor", - experienceLevel: ["intermediate", "advanced"], - }, - { - packageName: "postman", - platforms: ["windows", "macos", "ubuntu", "debian"], - priority: 6, - reason: "API development and testing tool", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, + +type PlatformId = "windows" | "macos" | "ubuntu" | "debian" | "arch" | "fedora"; + +export const PACKAGE_PRESETS: Record> = { + windows: { + development: [ + "git", + "code", // Visual Studio Code + "nodejs", + "python", + "docker-desktop", + "postman", + ], + design: [ + "gimp", + "inkscape", + "blender", + ], + multimedia: [ + "vlc", + "audacity", + "obs-studio", + ], + "system-tools": [ + "7zip", + "powertoys", + "everything", + ], + gaming: [ + "steam", + "discord", + ], + productivity: [ + "notion", + "obsidian", + "slack", + ], + education: [ + "anki", ], }, - { - category: "design", - description: "Tools for graphic design, UI/UX, and creative work", - packages: [ - { - packageName: "gimp", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 9, - reason: "Free and open-source image editor", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "inkscape", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 8, - reason: "Professional vector graphics editor", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "blender", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 8, - reason: "3D creation suite", - experienceLevel: ["intermediate", "advanced"], - }, - { - packageName: "figma", - platforms: ["windows", "macos"], - priority: 9, - reason: "Collaborative interface design tool", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "krita", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 7, - reason: "Digital painting application", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, + + macos: { + development: [ + "git", + "code", // Visual Studio Code + "nodejs", + "python", + "docker", + "postman", + ], + design: [ + "gimp", + "inkscape", + "blender", + ], + multimedia: [ + "vlc", + "audacity", + "obs", + ], + "system-tools": [ + "rectangle", + "the-unarchiver", + ], + gaming: [ + "steam", + "discord", + ], + productivity: [ + "notion", + "obsidian", + "slack", + ], + education: [ + "anki", ], }, - { - category: "multimedia", - description: "Audio, video editing and media management tools", - packages: [ - { - packageName: "vlc", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 10, - reason: "Versatile media player", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "obs-studio", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 9, - reason: "Video recording and live streaming", - experienceLevel: ["intermediate", "advanced"], - }, - { - packageName: "audacity", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 8, - reason: "Audio editing software", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "ffmpeg", - platforms: ["ubuntu", "debian", "arch", "fedora", "macos"], - priority: 8, - reason: "Complete multimedia framework", - experienceLevel: ["intermediate", "advanced"], - }, - { - packageName: "handbrake", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 7, - reason: "Video transcoder", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "kdenlive", - platforms: ["ubuntu", "debian", "arch", "fedora"], - priority: 7, - reason: "Video editing software", - experienceLevel: ["intermediate", "advanced"], - }, + + ubuntu: { + development: [ + "git", + "code", // Visual Studio Code + "nodejs", + "python3", + "docker.io", + "curl", + ], + design: [ + "gimp", + "inkscape", + "blender", + ], + multimedia: [ + "vlc", + "audacity", + "obs-studio", + ], + "system-tools": [ + "htop", + "neofetch", + "tldr", + ], + gaming: [ + "steam", + "discord", + ], + productivity: [ + "libreoffice", + "thunderbird", + ], + education: [ + "anki", ], }, - { - category: "system-tools", - description: "System administration, security and utilities", - packages: [ - { - packageName: "htop", - platforms: ["ubuntu", "debian", "arch", "fedora", "macos"], - priority: 9, - reason: "Interactive process viewer", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "tmux", - platforms: ["ubuntu", "debian", "arch", "fedora", "macos"], - priority: 8, - reason: "Terminal multiplexer", - experienceLevel: ["intermediate", "advanced"], - }, - { - packageName: "wget", - platforms: ["ubuntu", "debian", "arch", "fedora", "macos"], - priority: 8, - reason: "Network downloader", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "neofetch", - platforms: ["ubuntu", "debian", "arch", "fedora", "macos"], - priority: 6, - reason: "System information tool", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "wireshark", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 7, - reason: "Network protocol analyzer", - experienceLevel: ["advanced"], - }, - { - packageName: "gparted", - platforms: ["ubuntu", "debian", "arch", "fedora"], - priority: 6, - reason: "Partition editor", - experienceLevel: ["intermediate", "advanced"], - }, + + debian: { + development: [ + "git", + "code", + "nodejs", + "python3", + "docker.io", + "curl", + ], + design: [ + "gimp", + "inkscape", + "blender", + ], + multimedia: [ + "vlc", + "audacity", + "obs-studio", + ], + "system-tools": [ + "htop", + "neofetch", + "tldr", + ], + gaming: [ + "steam", + "discord", + ], + productivity: [ + "libreoffice", + "thunderbird", + ], + education: [ + "anki", ], }, - { - category: "gaming", - description: "Gaming platforms and related tools", - packages: [ - { - packageName: "steam", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 10, - reason: "Gaming platform", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "discord", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 9, - reason: "Voice and chat for gamers", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "lutris", - platforms: ["ubuntu", "debian", "arch", "fedora"], - priority: 7, - reason: "Open gaming platform", - experienceLevel: ["intermediate", "advanced"], - }, - { - packageName: "wine", - platforms: ["ubuntu", "debian", "arch", "fedora", "macos"], - priority: 6, - reason: "Windows compatibility layer", - experienceLevel: ["advanced"], - }, + + arch: { + development: [ + "git", + "visual-studio-code-bin", + "nodejs", + "python", + "docker", + "postman-bin", + ], + design: [ + "gimp", + "inkscape", + "blender", + ], + multimedia: [ + "vlc", + "audacity", + "obs-studio", + ], + "system-tools": [ + "htop", + "neofetch", + "tldr", + ], + gaming: [ + "steam", + "discord", + ], + productivity: [ + "libreoffice-fresh", + "thunderbird", + ], + education: [ + "anki", ], }, - { - category: "productivity", - description: "Office, note-taking and productivity tools", - packages: [ - { - packageName: "libreoffice", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 10, - reason: "Free office suite", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "thunderbird", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 8, - reason: "Email client", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "notion", - platforms: ["windows", "macos"], - priority: 9, - reason: "All-in-one workspace", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "obsidian", - platforms: ["windows", "macos", "ubuntu", "debian"], - priority: 8, - reason: "Knowledge base and note-taking", - experienceLevel: ["intermediate", "advanced"], - }, - { - packageName: "keepassxc", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 7, - reason: "Password manager", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, + + fedora: { + development: [ + "git", + "code", + "nodejs", + "python3", + "docker", + "curl", + ], + design: [ + "gimp", + "inkscape", + "blender", + ], + multimedia: [ + "vlc", + "audacity", + "obs-studio", + ], + "system-tools": [ + "htop", + "neofetch", + "tldr", + ], + gaming: [ + "steam", + "discord", + ], + productivity: [ + "libreoffice", + "thunderbird", + ], + education: [ + "anki", ], }, - { - category: "education", - description: "Educational and scientific software", - packages: [ - { - packageName: "anki", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 9, - reason: "Flashcard application for learning", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "stellarium", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 7, - reason: "Planetarium software", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - { - packageName: "octave", - platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"], - priority: 7, - reason: "Scientific programming language", - experienceLevel: ["intermediate", "advanced"], - }, - { - packageName: "geogebra", - platforms: ["windows", "macos", "ubuntu", "debian"], - priority: 8, - reason: "Interactive mathematics software", - experienceLevel: ["beginner", "intermediate", "advanced"], - }, - ], - }, -]; +}; /** - * Get presets for specific categories + * Get package names for a specific platform and categories */ -export function getPresetsForCategories( - categories: string[] -): CategoryPreset[] { - return RECOMMENDATION_PRESETS.filter((preset) => - categories.includes(preset.category) - ); -} - -/** - * Get all package names from presets for a specific platform - */ -export function getPresetPackageNames( - categories: string[], - platformId: string +export function getPackagesForPlatform( + platformId: string, + categories: UserCategory[] ): string[] { - const presets = getPresetsForCategories(categories); - const packageNames = new Set(); + const platform = PACKAGE_PRESETS[platformId as PlatformId]; + if (!platform) return []; - presets.forEach((preset) => { - preset.packages.forEach((pkg) => { - if (pkg.platforms.includes(platformId)) { - packageNames.add(pkg.packageName); - } - }); + const packages: string[] = []; + categories.forEach(category => { + const categoryPackages = platform[category] || []; + packages.push(...categoryPackages); }); - return Array.from(packageNames); -} - -/** - * Get preset priority for a package - */ -export function getPresetPriority( - packageName: string, - categories: string[], - platformId: string -): number | null { - const presets = getPresetsForCategories(categories); - - for (const preset of presets) { - const pkg = preset.packages.find( - (p) => p.packageName === packageName && p.platforms.includes(platformId) - ); - if (pkg) { - return pkg.priority; - } - } - - return null; -} - -/** - * Get recommendation reason for a package - */ -export function getRecommendationReason( - packageName: string, - categories: string[] -): string | null { - const presets = getPresetsForCategories(categories); - - for (const preset of presets) { - const pkg = preset.packages.find((p) => p.packageName === packageName); - if (pkg) { - return pkg.reason; - } - } - - return null; + return packages; } diff --git a/src/hooks/useRecommendationProfile.ts b/src/hooks/useRecommendationProfile.ts index 52a1f1d..7957814 100644 --- a/src/hooks/useRecommendationProfile.ts +++ b/src/hooks/useRecommendationProfile.ts @@ -68,7 +68,6 @@ function getDefaultProfile(): UserProfile { categories: [], detectedOS: detectOS(), selectedOS: undefined, - experienceLevel: "beginner", hasCompletedOnboarding: false, createdAt: new Date().toISOString(), lastUpdated: new Date().toISOString(), @@ -166,13 +165,7 @@ export function useRecommendationProfile() { [saveProfile] ); - // Update experience level - const updateExperienceLevel = useCallback( - (level: ExperienceLevel) => { - return saveProfile({ experienceLevel: level }); - }, - [saveProfile] - ); + // Mark onboarding as completed const completeOnboarding = useCallback(() => { @@ -208,7 +201,6 @@ export function useRecommendationProfile() { saveProfile, updateCategories, updateSelectedOS, - updateExperienceLevel, completeOnboarding, resetProfile, getEffectiveOS, diff --git a/src/services/recommendationService.ts b/src/services/recommendationService.ts index 822162f..058d830 100644 --- a/src/services/recommendationService.ts +++ b/src/services/recommendationService.ts @@ -6,10 +6,7 @@ import { UserCategory, ExperienceLevel, } from "@/types/recommendations"; -import { - getPresetPackageNames, - RECOMMENDATION_PRESETS, -} from "@/data/recommendationPresets"; +import { getPackagesForPlatform } from "@/data/recommendationPresets"; @@ -23,50 +20,32 @@ export class RecommendationService { const { platform_id, categories, experienceLevel, limit = 20 } = request; // Step 1: Get preset package names for the user's categories and platform - const presetPackageNames = getPresetPackageNames(categories, platform_id); + const presetPackageNames = getPackagesForPlatform(platform_id, categories); - // Step 2: Fetch packages from database with category tracking - // Map to track which category each package came from + // Step 2: Fetch packages from database const packageCategoryMap = new Map(); - // First, get preset packages (tagged with their categories) - const presetPackagesWithCategories = - await this.fetchPresetPackagesWithCategories( - categories, - platform_id, - packageCategoryMap - ); - - // Then, get additional packages from categories - const categoryPackagesMap = await this.fetchCategoryPackagesWithTracking( - categories, + // Fetch preset packages + const presetPackages = await this.fetchPresetPackages( + presetPackageNames, platform_id, - limit * 2, // Fetch more to ensure we have enough after filtering + categories, packageCategoryMap ); - // Step 3: Combine and deduplicate - const allPackages = this.deduplicatePackages([ - ...presetPackagesWithCategories, - ...categoryPackagesMap, - ]); - - // Step 4: Score and rank packages - distribute categories evenly for untracked packages - const scoredPackages = allPackages.map((pkg, index) => { - // If not in map, distribute evenly across categories - const matchedCategory = - packageCategoryMap.get(pkg.id) || categories[index % categories.length]; + // Step 3: Score and rank packages + const scoredPackages = presetPackages.map((pkg) => { + const matchedCategory = packageCategoryMap.get(pkg.id) || categories[0]; return this.scorePackage( pkg, categories, platform_id, presetPackageNames, - experienceLevel, matchedCategory ); }); - // Step 5: Sort by score and limit results + // Step 4: Sort by score and limit results scoredPackages.sort( (a, b) => b.recommendationScore - a.recommendationScore ); @@ -74,71 +53,24 @@ export class RecommendationService { return scoredPackages.slice(0, limit); } - /** - * Fetch packages that match preset names with category tracking - */ - private static async fetchPresetPackagesWithCategories( - categories: UserCategory[], - platformId: string, - categoryMap: Map - ): Promise { - const packages: Package[] = []; - - for (const category of categories) { - const preset = RECOMMENDATION_PRESETS.find( - (p) => p.category === category - ); - if (!preset) continue; - - const categoryPackageNames = preset.packages - .filter((p) => p.platforms.includes(platformId)) - .map((p) => p.packageName); - - for (const name of categoryPackageNames) { - const result = await PackageService.getMany({ - platform_id: platformId, - search: name, - limit: 5, - sort_by: "popularity_score", - sort_order: "desc", - }); - - const exactMatch = result.packages.find( - (pkg) => pkg.name.toLowerCase() === name.toLowerCase() - ); - - if (exactMatch) { - packages.push(exactMatch); - categoryMap.set(exactMatch.id, category); - } else if (result.packages.length > 0) { - packages.push(result.packages[0]); - categoryMap.set(result.packages[0].id, category); - } - } - } - - return packages; - } - /** * Fetch packages that match preset names - * Optimized: Uses single query instead of N queries */ private static async fetchPresetPackages( packageNames: string[], - platformId: string + platformId: string, + categories: UserCategory[], + categoryMap: Map ): Promise { if (packageNames.length === 0) { return []; } try { - // Fetch all preset packages in one query const packages: Package[] = []; + let categoryIndex = 0; // Search for each package name (case-insensitive) - // Note: Current API doesn't support bulk name filtering, - // so we optimize by fetching larger batches and filtering for (const name of packageNames) { const result = await PackageService.getMany({ platform_id: platformId, @@ -155,9 +87,14 @@ export class RecommendationService { if (exactMatch) { packages.push(exactMatch); + // Distribute categories evenly + categoryMap.set(exactMatch.id, categories[categoryIndex % categories.length]); + categoryIndex++; } else if (result.packages.length > 0) { // If no exact match, take the first result (most popular match) packages.push(result.packages[0]); + categoryMap.set(result.packages[0].id, categories[categoryIndex % categories.length]); + categoryIndex++; } } @@ -168,105 +105,7 @@ export class RecommendationService { } } - /** - * Fetch packages based on categories with tracking - */ - private static async fetchCategoryPackagesWithTracking( - categories: UserCategory[], - platformId: string, - limit: number, - categoryMap: Map - ): Promise { - try { - const allPackages: Package[] = []; - const seenIds = new Set(); - const perCategory = Math.ceil(limit / categories.length); - for (const category of categories) { - const result = await PackageService.getMany({ - platform_id: platformId, - limit: perCategory, - sort_by: "popularity_score", - sort_order: "desc", - }); - - // Add packages without duplicates and tag with category - for (const pkg of result.packages) { - if (!seenIds.has(pkg.id)) { - seenIds.add(pkg.id); - allPackages.push(pkg); - // Tag this package with the category - if (!categoryMap.has(pkg.id)) { - categoryMap.set(pkg.id, category); - } - } - } - } - - return allPackages; - } catch (error) { - console.error("Error fetching category packages:", error); - return []; - } - } - - /** - * Fetch packages based on categories - * Now properly uses database category filtering - */ - private static async fetchCategoryPackages( - categories: UserCategory[], - platformId: string, - limit: number - ): Promise { - try { - // Map user categories to database category names (from schema.sql) - const categoryMap: Record = { - development: ["Development", "Internet"], - design: ["Graphics"], - multimedia: ["Multimedia"], - "system-tools": ["System", "Utilities"], - gaming: ["Games"], - productivity: ["Office"], - education: ["Science"], - }; - - // Get category IDs from database - const allPackages: Package[] = []; - const seenIds = new Set(); - - for (const category of categories) { - const dbCategoryNames = categoryMap[category] || []; - - // Fetch packages for each DB category - for (const dbCategoryName of dbCategoryNames) { - // Note: We need to fetch by search since API doesn't expose category names directly - // This is a workaround until we add category name filtering to API - const result = await PackageService.getMany({ - platform_id: platformId, - limit: Math.ceil( - limit / (categories.length * dbCategoryNames.length) - ), - sort_by: "popularity_score", - sort_order: "desc", - }); - - // Add packages without duplicates - for (const pkg of result.packages) { - if (!seenIds.has(pkg.id)) { - seenIds.add(pkg.id); - allPackages.push(pkg); - } - } - } - } - - return allPackages; - } catch (error) { - console.error("Error fetching category packages:", error); - return []; - } - } /** * Remove duplicate packages (by ID) @@ -282,9 +121,6 @@ export class RecommendationService { }); } - /** - * Score a package based on multiple factors - */ /** * Score a package based on simplified factors (popularity & preset) */ @@ -293,7 +129,6 @@ export class RecommendationService { categories: UserCategory[], platformId: string, presetPackageNames: string[], - experienceLevel?: ExperienceLevel, matchedCategory?: UserCategory ): RecommendedPackage { const isPresetMatch = presetPackageNames.includes(pkg.name);