refactor: standardize string quotes and improve code readability across multiple files

- Updated string quotes from single to double in useRecommendationProfile.ts, client.ts, recommendationService.ts, and recommendations.ts for consistency.
- Enhanced error handling and logging in API client and recommendation service.
- Improved code structure and formatting for better readability and maintainability.
- Ensured consistent use of semicolons and spacing throughout the codebase.
This commit is contained in:
ersaayan
2025-11-22 16:21:15 +03:00
parent 73f9248e98
commit bcf8652728
10 changed files with 1129 additions and 1054 deletions
+88 -62
View File
@@ -1,70 +1,82 @@
import { NextRequest, NextResponse } from 'next/server' import { NextRequest, NextResponse } from "next/server";
import { RecommendationService } from '@/services/recommendationService' import { RecommendationService } from "@/services/recommendationService";
import { RecommendationRequest } from '@/types/recommendations' import { RecommendationRequest } from "@/types/recommendations";
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
const body: RecommendationRequest = await request.json() const body: RecommendationRequest = await request.json();
// Validate required fields // Validate required fields
if (!body.platform_id) { if (!body.platform_id) {
return NextResponse.json( return NextResponse.json(
{ error: 'platform_id is required' }, { error: "platform_id is required" },
{ status: 400 } { status: 400 }
) );
} }
if (!body.categories || body.categories.length === 0) { if (!body.categories || body.categories.length === 0) {
return NextResponse.json( return NextResponse.json(
{ error: 'At least one category is required' }, { error: "At least one category is required" },
{ status: 400 } { status: 400 }
) );
} }
// Validate platform_id // Validate platform_id
const validPlatforms = ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'] const validPlatforms = [
"windows",
"macos",
"ubuntu",
"debian",
"arch",
"fedora",
];
if (!validPlatforms.includes(body.platform_id)) { if (!validPlatforms.includes(body.platform_id)) {
return NextResponse.json( return NextResponse.json(
{ error: `Invalid platform_id. Must be one of: ${validPlatforms.join(', ')}` }, {
error: `Invalid platform_id. Must be one of: ${validPlatforms.join(
", "
)}`,
},
{ status: 400 } { status: 400 }
) );
} }
// Validate categories // Validate categories
const validCategories = [ const validCategories = [
'development', "development",
'design', "design",
'multimedia', "multimedia",
'system-tools', "system-tools",
'gaming', "gaming",
'productivity', "productivity",
'education' "education",
] ];
const invalidCategories = body.categories.filter( const invalidCategories = body.categories.filter(
cat => !validCategories.includes(cat) (cat) => !validCategories.includes(cat)
) );
if (invalidCategories.length > 0) { if (invalidCategories.length > 0) {
return NextResponse.json( return NextResponse.json(
{ {
error: `Invalid categories: ${invalidCategories.join(', ')}`, error: `Invalid categories: ${invalidCategories.join(", ")}`,
validCategories validCategories,
}, },
{ status: 400 } { status: 400 }
) );
} }
// Set default limit // Set default limit
const limit = body.limit && body.limit > 0 && body.limit <= 50 const limit =
? body.limit body.limit && body.limit > 0 && body.limit <= 50 ? body.limit : 20;
: 20
// Generate recommendations // Generate recommendations
const recommendations = await RecommendationService.generateRecommendations({ const recommendations = await RecommendationService.generateRecommendations(
{
platform_id: body.platform_id, platform_id: body.platform_id,
categories: body.categories, categories: body.categories,
experienceLevel: body.experienceLevel, experienceLevel: body.experienceLevel,
limit limit,
}) }
);
return NextResponse.json({ return NextResponse.json({
recommendations, recommendations,
@@ -72,68 +84,82 @@ export async function POST(request: NextRequest) {
userProfile: { userProfile: {
categories: body.categories, categories: body.categories,
platform: body.platform_id, platform: body.platform_id,
experienceLevel: body.experienceLevel experienceLevel: body.experienceLevel,
} },
}) });
} catch (error) { } catch (error) {
console.error('Error generating recommendations:', error) console.error("Error generating recommendations:", error);
return NextResponse.json( return NextResponse.json(
{ {
error: 'Failed to generate recommendations', error: "Failed to generate recommendations",
details: error instanceof Error ? error.message : 'Unknown error' details: error instanceof Error ? error.message : "Unknown error",
}, },
{ status: 500 } { status: 500 }
) );
} }
} }
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
const { searchParams } = new URL(request.url) const { searchParams } = new URL(request.url);
const platformId = searchParams.get('platform_id') const platformId = searchParams.get("platform_id");
const categoriesParam = searchParams.get('categories') const categoriesParam = searchParams.get("categories");
const experienceLevel = searchParams.get('experience_level') const experienceLevel = searchParams.get("experience_level");
const limit = searchParams.get('limit') const limit = searchParams.get("limit");
// Validate required fields // Validate required fields
if (!platformId) { if (!platformId) {
return NextResponse.json( return NextResponse.json(
{ error: 'platform_id query parameter is required' }, { error: "platform_id query parameter is required" },
{ status: 400 } { status: 400 }
) );
} }
if (!categoriesParam) { if (!categoriesParam) {
return NextResponse.json( return NextResponse.json(
{ error: 'categories query parameter is required (comma-separated)' }, { error: "categories query parameter is required (comma-separated)" },
{ status: 400 } { status: 400 }
) );
} }
// Parse categories // Parse categories
const categories = categoriesParam.split(',').map(c => c.trim()) const categories = categoriesParam.split(",").map((c) => c.trim());
// Validate platform_id // Validate platform_id
const validPlatforms = ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'] const validPlatforms = [
"windows",
"macos",
"ubuntu",
"debian",
"arch",
"fedora",
];
if (!validPlatforms.includes(platformId)) { if (!validPlatforms.includes(platformId)) {
return NextResponse.json( return NextResponse.json(
{ error: `Invalid platform_id. Must be one of: ${validPlatforms.join(', ')}` }, {
error: `Invalid platform_id. Must be one of: ${validPlatforms.join(
", "
)}`,
},
{ status: 400 } { status: 400 }
) );
} }
// Set default limit // Set default limit
const parsedLimit = limit && parseInt(limit) > 0 && parseInt(limit) <= 50 const parsedLimit =
limit && parseInt(limit) > 0 && parseInt(limit) <= 50
? parseInt(limit) ? parseInt(limit)
: 20 : 20;
// Generate recommendations // Generate recommendations
const recommendations = await RecommendationService.generateRecommendations({ const recommendations = await RecommendationService.generateRecommendations(
{
platform_id: platformId, platform_id: platformId,
categories: categories as any, categories: categories as any,
experienceLevel: experienceLevel as any, experienceLevel: experienceLevel as any,
limit: parsedLimit limit: parsedLimit,
}) }
);
return NextResponse.json({ return NextResponse.json({
recommendations, recommendations,
@@ -141,17 +167,17 @@ export async function GET(request: NextRequest) {
userProfile: { userProfile: {
categories, categories,
platform: platformId, platform: platformId,
experienceLevel experienceLevel,
} },
}) });
} catch (error) { } catch (error) {
console.error('Error generating recommendations:', error) console.error("Error generating recommendations:", error);
return NextResponse.json( return NextResponse.json(
{ {
error: 'Failed to generate recommendations', error: "Failed to generate recommendations",
details: error instanceof Error ? error.message : 'Unknown error' details: error instanceof Error ? error.message : "Unknown error",
}, },
{ status: 500 } { status: 500 }
) );
} }
} }
+4 -8
View File
@@ -113,8 +113,7 @@ export function OnboardingModal({
{[1, 2, 3].map(i => ( {[1, 2, 3].map(i => (
<div <div
key={i} key={i}
className={`h-2 flex-1 rounded-full transition-colors ${ className={`h-2 flex-1 rounded-full transition-colors ${i <= step ? 'bg-primary' : 'bg-secondary'
i <= step ? 'bg-primary' : 'bg-secondary'
}`} }`}
/> />
))} ))}
@@ -139,8 +138,7 @@ export function OnboardingModal({
<button <button
key={preset.category} key={preset.category}
onClick={() => handleCategoryToggle(preset.category)} onClick={() => handleCategoryToggle(preset.category)}
className={`p-4 rounded-lg border-2 text-left transition-all hover:scale-105 ${ className={`p-4 rounded-lg border-2 text-left transition-all hover:scale-105 ${selectedCategories.includes(preset.category)
selectedCategories.includes(preset.category)
? 'border-primary bg-primary/10' ? 'border-primary bg-primary/10'
: 'border-border hover:border-primary/50' : 'border-border hover:border-primary/50'
}`} }`}
@@ -190,8 +188,7 @@ export function OnboardingModal({
<button <button
key={platform.id} key={platform.id}
onClick={() => setSelectedOS(platform.id)} onClick={() => setSelectedOS(platform.id)}
className={`p-4 rounded-lg border-2 text-center transition-all hover:scale-105 ${ className={`p-4 rounded-lg border-2 text-center transition-all hover:scale-105 ${selectedOS === platform.id
selectedOS === platform.id
? 'border-primary bg-primary/10' ? 'border-primary bg-primary/10'
: 'border-border hover:border-primary/50' : 'border-border hover:border-primary/50'
}`} }`}
@@ -221,8 +218,7 @@ export function OnboardingModal({
<button <button
key={level} key={level}
onClick={() => setExperienceLevel(level)} onClick={() => setExperienceLevel(level)}
className={`w-full p-4 rounded-lg border-2 text-left transition-all hover:scale-[1.02] ${ className={`w-full p-4 rounded-lg border-2 text-left transition-all hover:scale-[1.02] ${experienceLevel === level
experienceLevel === level
? 'border-primary bg-primary/10' ? 'border-primary bg-primary/10'
: 'border-border hover:border-primary/50' : 'border-border hover:border-primary/50'
}`} }`}
+1 -2
View File
@@ -164,8 +164,7 @@ export function RecommendationsSection({
{recommendations.map(pkg => ( {recommendations.map(pkg => (
<Card <Card
key={pkg.id} key={pkg.id}
className={`relative overflow-hidden transition-all hover:shadow-lg cursor-pointer ${ className={`relative overflow-hidden transition-all hover:shadow-lg cursor-pointer ${isPackageSelected(pkg) ? 'ring-2 ring-primary' : ''
isPackageSelected(pkg) ? 'ring-2 ring-primary' : ''
}`} }`}
onClick={() => onPackageToggle(pkg)} onClick={() => onPackageToggle(pkg)}
> >
+220 -218
View File
@@ -1,4 +1,4 @@
import { CategoryPreset } from '@/types/recommendations' import { CategoryPreset } from "@/types/recommendations";
/** /**
* Curated package recommendations for each user category * Curated package recommendations for each user category
@@ -6,343 +6,345 @@ import { CategoryPreset } from '@/types/recommendations'
*/ */
export const RECOMMENDATION_PRESETS: CategoryPreset[] = [ export const RECOMMENDATION_PRESETS: CategoryPreset[] = [
{ {
category: 'development', category: "development",
description: 'Essential tools for software development', description: "Essential tools for software development",
icon: '💻', icon: "💻",
packages: [ packages: [
{ {
packageName: 'git', packageName: "git",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 10, priority: 10,
reason: 'Version control system essential for all developers', reason: "Version control system essential for all developers",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'code', packageName: "code",
platforms: ['windows', 'macos', 'ubuntu', 'debian'], platforms: ["windows", "macos", "ubuntu", "debian"],
priority: 9, priority: 9,
reason: 'Visual Studio Code - Popular code editor', reason: "Visual Studio Code - Popular code editor",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'visual-studio-code', packageName: "visual-studio-code",
platforms: ['arch', 'fedora'], platforms: ["arch", "fedora"],
priority: 9, priority: 9,
reason: 'Visual Studio Code - Popular code editor', reason: "Visual Studio Code - Popular code editor",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'nodejs', packageName: "nodejs",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 8, priority: 8,
reason: 'JavaScript runtime for modern web development', reason: "JavaScript runtime for modern web development",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'python3', packageName: "python3",
platforms: ['ubuntu', 'debian', 'arch', 'fedora'], platforms: ["ubuntu", "debian", "arch", "fedora"],
priority: 8, priority: 8,
reason: 'Python programming language', reason: "Python programming language",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'python', packageName: "python",
platforms: ['windows', 'macos'], platforms: ["windows", "macos"],
priority: 8, priority: 8,
reason: 'Python programming language', reason: "Python programming language",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'docker', packageName: "docker",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 7, priority: 7,
reason: 'Containerization platform for development', reason: "Containerization platform for development",
experienceLevel: ['intermediate', 'advanced'] experienceLevel: ["intermediate", "advanced"],
}, },
{ {
packageName: 'curl', packageName: "curl",
platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'], platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
priority: 7, priority: 7,
reason: 'Command-line tool for transferring data', reason: "Command-line tool for transferring data",
experienceLevel: ['intermediate', 'advanced'] experienceLevel: ["intermediate", "advanced"],
}, },
{ {
packageName: 'vim', packageName: "vim",
platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'], platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
priority: 6, priority: 6,
reason: 'Powerful text editor', reason: "Powerful text editor",
experienceLevel: ['intermediate', 'advanced'] experienceLevel: ["intermediate", "advanced"],
}, },
{ {
packageName: 'postman', packageName: "postman",
platforms: ['windows', 'macos', 'ubuntu', 'debian'], platforms: ["windows", "macos", "ubuntu", "debian"],
priority: 6, priority: 6,
reason: 'API development and testing tool', reason: "API development and testing tool",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
} },
] ],
}, },
{ {
category: 'design', category: "design",
description: 'Tools for graphic design, UI/UX, and creative work', description: "Tools for graphic design, UI/UX, and creative work",
icon: '🎨', icon: "🎨",
packages: [ packages: [
{ {
packageName: 'gimp', packageName: "gimp",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 9, priority: 9,
reason: 'Free and open-source image editor', reason: "Free and open-source image editor",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'inkscape', packageName: "inkscape",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 8, priority: 8,
reason: 'Professional vector graphics editor', reason: "Professional vector graphics editor",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'blender', packageName: "blender",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 8, priority: 8,
reason: '3D creation suite', reason: "3D creation suite",
experienceLevel: ['intermediate', 'advanced'] experienceLevel: ["intermediate", "advanced"],
}, },
{ {
packageName: 'figma', packageName: "figma",
platforms: ['windows', 'macos'], platforms: ["windows", "macos"],
priority: 9, priority: 9,
reason: 'Collaborative interface design tool', reason: "Collaborative interface design tool",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'krita', packageName: "krita",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 7, priority: 7,
reason: 'Digital painting application', reason: "Digital painting application",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
} },
] ],
}, },
{ {
category: 'multimedia', category: "multimedia",
description: 'Audio, video editing and media management tools', description: "Audio, video editing and media management tools",
icon: '🎬', icon: "🎬",
packages: [ packages: [
{ {
packageName: 'vlc', packageName: "vlc",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 10, priority: 10,
reason: 'Versatile media player', reason: "Versatile media player",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'obs-studio', packageName: "obs-studio",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 9, priority: 9,
reason: 'Video recording and live streaming', reason: "Video recording and live streaming",
experienceLevel: ['intermediate', 'advanced'] experienceLevel: ["intermediate", "advanced"],
}, },
{ {
packageName: 'audacity', packageName: "audacity",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 8, priority: 8,
reason: 'Audio editing software', reason: "Audio editing software",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'ffmpeg', packageName: "ffmpeg",
platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'], platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
priority: 8, priority: 8,
reason: 'Complete multimedia framework', reason: "Complete multimedia framework",
experienceLevel: ['intermediate', 'advanced'] experienceLevel: ["intermediate", "advanced"],
}, },
{ {
packageName: 'handbrake', packageName: "handbrake",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 7, priority: 7,
reason: 'Video transcoder', reason: "Video transcoder",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'kdenlive', packageName: "kdenlive",
platforms: ['ubuntu', 'debian', 'arch', 'fedora'], platforms: ["ubuntu", "debian", "arch", "fedora"],
priority: 7, priority: 7,
reason: 'Video editing software', reason: "Video editing software",
experienceLevel: ['intermediate', 'advanced'] experienceLevel: ["intermediate", "advanced"],
} },
] ],
}, },
{ {
category: 'system-tools', category: "system-tools",
description: 'System administration, security and utilities', description: "System administration, security and utilities",
icon: '⚙️', icon: "⚙️",
packages: [ packages: [
{ {
packageName: 'htop', packageName: "htop",
platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'], platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
priority: 9, priority: 9,
reason: 'Interactive process viewer', reason: "Interactive process viewer",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'tmux', packageName: "tmux",
platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'], platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
priority: 8, priority: 8,
reason: 'Terminal multiplexer', reason: "Terminal multiplexer",
experienceLevel: ['intermediate', 'advanced'] experienceLevel: ["intermediate", "advanced"],
}, },
{ {
packageName: 'wget', packageName: "wget",
platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'], platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
priority: 8, priority: 8,
reason: 'Network downloader', reason: "Network downloader",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'neofetch', packageName: "neofetch",
platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'], platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
priority: 6, priority: 6,
reason: 'System information tool', reason: "System information tool",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'wireshark', packageName: "wireshark",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 7, priority: 7,
reason: 'Network protocol analyzer', reason: "Network protocol analyzer",
experienceLevel: ['advanced'] experienceLevel: ["advanced"],
}, },
{ {
packageName: 'gparted', packageName: "gparted",
platforms: ['ubuntu', 'debian', 'arch', 'fedora'], platforms: ["ubuntu", "debian", "arch", "fedora"],
priority: 6, priority: 6,
reason: 'Partition editor', reason: "Partition editor",
experienceLevel: ['intermediate', 'advanced'] experienceLevel: ["intermediate", "advanced"],
} },
] ],
}, },
{ {
category: 'gaming', category: "gaming",
description: 'Gaming platforms and related tools', description: "Gaming platforms and related tools",
icon: '🎮', icon: "🎮",
packages: [ packages: [
{ {
packageName: 'steam', packageName: "steam",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 10, priority: 10,
reason: 'Gaming platform', reason: "Gaming platform",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'discord', packageName: "discord",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 9, priority: 9,
reason: 'Voice and chat for gamers', reason: "Voice and chat for gamers",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'lutris', packageName: "lutris",
platforms: ['ubuntu', 'debian', 'arch', 'fedora'], platforms: ["ubuntu", "debian", "arch", "fedora"],
priority: 7, priority: 7,
reason: 'Open gaming platform', reason: "Open gaming platform",
experienceLevel: ['intermediate', 'advanced'] experienceLevel: ["intermediate", "advanced"],
}, },
{ {
packageName: 'wine', packageName: "wine",
platforms: ['ubuntu', 'debian', 'arch', 'fedora', 'macos'], platforms: ["ubuntu", "debian", "arch", "fedora", "macos"],
priority: 6, priority: 6,
reason: 'Windows compatibility layer', reason: "Windows compatibility layer",
experienceLevel: ['advanced'] experienceLevel: ["advanced"],
} },
] ],
}, },
{ {
category: 'productivity', category: "productivity",
description: 'Office, note-taking and productivity tools', description: "Office, note-taking and productivity tools",
icon: '📝', icon: "📝",
packages: [ packages: [
{ {
packageName: 'libreoffice', packageName: "libreoffice",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 10, priority: 10,
reason: 'Free office suite', reason: "Free office suite",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'thunderbird', packageName: "thunderbird",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 8, priority: 8,
reason: 'Email client', reason: "Email client",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'notion', packageName: "notion",
platforms: ['windows', 'macos'], platforms: ["windows", "macos"],
priority: 9, priority: 9,
reason: 'All-in-one workspace', reason: "All-in-one workspace",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'obsidian', packageName: "obsidian",
platforms: ['windows', 'macos', 'ubuntu', 'debian'], platforms: ["windows", "macos", "ubuntu", "debian"],
priority: 8, priority: 8,
reason: 'Knowledge base and note-taking', reason: "Knowledge base and note-taking",
experienceLevel: ['intermediate', 'advanced'] experienceLevel: ["intermediate", "advanced"],
}, },
{ {
packageName: 'keepassxc', packageName: "keepassxc",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 7, priority: 7,
reason: 'Password manager', reason: "Password manager",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
} },
] ],
}, },
{ {
category: 'education', category: "education",
description: 'Educational and scientific software', description: "Educational and scientific software",
icon: '🎓', icon: "🎓",
packages: [ packages: [
{ {
packageName: 'anki', packageName: "anki",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 9, priority: 9,
reason: 'Flashcard application for learning', reason: "Flashcard application for learning",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'stellarium', packageName: "stellarium",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 7, priority: 7,
reason: 'Planetarium software', reason: "Planetarium software",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
}, },
{ {
packageName: 'octave', packageName: "octave",
platforms: ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora'], platforms: ["windows", "macos", "ubuntu", "debian", "arch", "fedora"],
priority: 7, priority: 7,
reason: 'Scientific programming language', reason: "Scientific programming language",
experienceLevel: ['intermediate', 'advanced'] experienceLevel: ["intermediate", "advanced"],
}, },
{ {
packageName: 'geogebra', packageName: "geogebra",
platforms: ['windows', 'macos', 'ubuntu', 'debian'], platforms: ["windows", "macos", "ubuntu", "debian"],
priority: 8, priority: 8,
reason: 'Interactive mathematics software', reason: "Interactive mathematics software",
experienceLevel: ['beginner', 'intermediate', 'advanced'] experienceLevel: ["beginner", "intermediate", "advanced"],
} },
] ],
} },
] ];
/** /**
* Get presets for specific categories * Get presets for specific categories
*/ */
export function getPresetsForCategories(categories: string[]): CategoryPreset[] { export function getPresetsForCategories(
return RECOMMENDATION_PRESETS.filter(preset => categories: string[]
): CategoryPreset[] {
return RECOMMENDATION_PRESETS.filter((preset) =>
categories.includes(preset.category) categories.includes(preset.category)
) );
} }
/** /**
@@ -352,18 +354,18 @@ export function getPresetPackageNames(
categories: string[], categories: string[],
platformId: string platformId: string
): string[] { ): string[] {
const presets = getPresetsForCategories(categories) const presets = getPresetsForCategories(categories);
const packageNames = new Set<string>() const packageNames = new Set<string>();
presets.forEach(preset => { presets.forEach((preset) => {
preset.packages.forEach(pkg => { preset.packages.forEach((pkg) => {
if (pkg.platforms.includes(platformId)) { if (pkg.platforms.includes(platformId)) {
packageNames.add(pkg.packageName) packageNames.add(pkg.packageName);
} }
}) });
}) });
return Array.from(packageNames) return Array.from(packageNames);
} }
/** /**
@@ -374,18 +376,18 @@ export function getPresetPriority(
categories: string[], categories: string[],
platformId: string platformId: string
): number | null { ): number | null {
const presets = getPresetsForCategories(categories) const presets = getPresetsForCategories(categories);
for (const preset of presets) { for (const preset of presets) {
const pkg = preset.packages.find( const pkg = preset.packages.find(
p => p.packageName === packageName && p.platforms.includes(platformId) (p) => p.packageName === packageName && p.platforms.includes(platformId)
) );
if (pkg) { if (pkg) {
return pkg.priority return pkg.priority;
} }
} }
return null return null;
} }
/** /**
@@ -395,14 +397,14 @@ export function getRecommendationReason(
packageName: string, packageName: string,
categories: string[] categories: string[]
): string | null { ): string | null {
const presets = getPresetsForCategories(categories) const presets = getPresetsForCategories(categories);
for (const preset of presets) { for (const preset of presets) {
const pkg = preset.packages.find(p => p.packageName === packageName) const pkg = preset.packages.find((p) => p.packageName === packageName);
if (pkg) { if (pkg) {
return pkg.reason return pkg.reason;
} }
} }
return null return null;
} }
+90 -74
View File
@@ -1,56 +1,60 @@
"use client" "use client";
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from "react";
import { UserProfile, UserCategory, ExperienceLevel } from '@/types/recommendations' import {
UserProfile,
UserCategory,
ExperienceLevel,
} from "@/types/recommendations";
const STORAGE_KEY = 'repohub_user_profile' const STORAGE_KEY = "repohub_user_profile";
/** /**
* Detect user's operating system from browser * Detect user's operating system from browser
*/ */
function detectOS(): string { function detectOS(): string {
if (typeof window === 'undefined') { if (typeof window === "undefined") {
return 'unknown' return "unknown";
} }
const userAgent = window.navigator.userAgent.toLowerCase() const userAgent = window.navigator.userAgent.toLowerCase();
const platform = window.navigator.platform.toLowerCase() const platform = window.navigator.platform.toLowerCase();
// Windows // Windows
if (userAgent.indexOf('win') !== -1 || platform.indexOf('win') !== -1) { if (userAgent.indexOf("win") !== -1 || platform.indexOf("win") !== -1) {
return 'windows' return "windows";
} }
// macOS // macOS
if ( if (
userAgent.indexOf('mac') !== -1 || userAgent.indexOf("mac") !== -1 ||
platform.indexOf('mac') !== -1 || platform.indexOf("mac") !== -1 ||
userAgent.indexOf('darwin') !== -1 userAgent.indexOf("darwin") !== -1
) { ) {
return 'macos' return "macos";
} }
// Linux distros // Linux distros
if (userAgent.indexOf('linux') !== -1 || platform.indexOf('linux') !== -1) { if (userAgent.indexOf("linux") !== -1 || platform.indexOf("linux") !== -1) {
// Try to detect specific distro from user agent (rare but possible) // Try to detect specific distro from user agent (rare but possible)
if (userAgent.indexOf('ubuntu') !== -1) { if (userAgent.indexOf("ubuntu") !== -1) {
return 'ubuntu' return "ubuntu";
} }
if (userAgent.indexOf('fedora') !== -1) { if (userAgent.indexOf("fedora") !== -1) {
return 'fedora' return "fedora";
} }
if (userAgent.indexOf('arch') !== -1) { if (userAgent.indexOf("arch") !== -1) {
return 'arch' return "arch";
} }
if (userAgent.indexOf('debian') !== -1) { if (userAgent.indexOf("debian") !== -1) {
return 'debian' return "debian";
} }
// Default to Ubuntu for generic Linux // Default to Ubuntu for generic Linux
return 'ubuntu' return "ubuntu";
} }
return 'unknown' return "unknown";
} }
/** /**
@@ -61,106 +65,118 @@ function getDefaultProfile(): UserProfile {
categories: [], categories: [],
detectedOS: detectOS(), detectedOS: detectOS(),
selectedOS: undefined, selectedOS: undefined,
experienceLevel: 'beginner', experienceLevel: "beginner",
hasCompletedOnboarding: false, hasCompletedOnboarding: false,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
lastUpdated: new Date().toISOString() lastUpdated: new Date().toISOString(),
} };
} }
/** /**
* Hook for managing user recommendation profile in localStorage * Hook for managing user recommendation profile in localStorage
*/ */
export function useRecommendationProfile() { export function useRecommendationProfile() {
const [profile, setProfile] = useState<UserProfile>(getDefaultProfile()) const [profile, setProfile] = useState<UserProfile>(getDefaultProfile());
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true);
// Load profile from localStorage on mount // Load profile from localStorage on mount
useEffect(() => { useEffect(() => {
try { try {
const stored = localStorage.getItem(STORAGE_KEY) const stored = localStorage.getItem(STORAGE_KEY);
if (stored) { if (stored) {
const parsed = JSON.parse(stored) as UserProfile const parsed = JSON.parse(stored) as UserProfile;
// Update detectedOS if it changed // Update detectedOS if it changed
const currentOS = detectOS() const currentOS = detectOS();
if (parsed.detectedOS !== currentOS) { if (parsed.detectedOS !== currentOS) {
parsed.detectedOS = currentOS parsed.detectedOS = currentOS;
} }
setProfile(parsed) setProfile(parsed);
} else { } else {
// First time user - save default profile // First time user - save default profile
const defaultProfile = getDefaultProfile() const defaultProfile = getDefaultProfile();
setProfile(defaultProfile) setProfile(defaultProfile);
localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultProfile)) localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultProfile));
} }
} catch (error) { } catch (error) {
console.error('Error loading user profile:', error) console.error("Error loading user profile:", error);
} finally { } finally {
setIsLoading(false) setIsLoading(false);
} }
}, []) }, []);
// Save profile to localStorage // Save profile to localStorage
const saveProfile = useCallback((newProfile: Partial<UserProfile>) => { const saveProfile = useCallback(
(newProfile: Partial<UserProfile>) => {
try { try {
const updated: UserProfile = { const updated: UserProfile = {
...profile, ...profile,
...newProfile, ...newProfile,
lastUpdated: new Date().toISOString() lastUpdated: new Date().toISOString(),
} };
setProfile(updated) setProfile(updated);
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated)) localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
return true return true;
} catch (error) { } catch (error) {
console.error('Error saving user profile:', error) console.error("Error saving user profile:", error);
return false return false;
} }
}, [profile]) },
[profile]
);
// Update categories // Update categories
const updateCategories = useCallback((categories: UserCategory[]) => { const updateCategories = useCallback(
return saveProfile({ categories }) (categories: UserCategory[]) => {
}, [saveProfile]) return saveProfile({ categories });
},
[saveProfile]
);
// Update selected OS (manual override) // Update selected OS (manual override)
const updateSelectedOS = useCallback((os: string) => { const updateSelectedOS = useCallback(
return saveProfile({ selectedOS: os }) (os: string) => {
}, [saveProfile]) return saveProfile({ selectedOS: os });
},
[saveProfile]
);
// Update experience level // Update experience level
const updateExperienceLevel = useCallback((level: ExperienceLevel) => { const updateExperienceLevel = useCallback(
return saveProfile({ experienceLevel: level }) (level: ExperienceLevel) => {
}, [saveProfile]) return saveProfile({ experienceLevel: level });
},
[saveProfile]
);
// Mark onboarding as completed // Mark onboarding as completed
const completeOnboarding = useCallback(() => { const completeOnboarding = useCallback(() => {
return saveProfile({ hasCompletedOnboarding: true }) return saveProfile({ hasCompletedOnboarding: true });
}, [saveProfile]) }, [saveProfile]);
// Reset profile // Reset profile
const resetProfile = useCallback(() => { const resetProfile = useCallback(() => {
try { try {
const defaultProfile = getDefaultProfile() const defaultProfile = getDefaultProfile();
setProfile(defaultProfile) setProfile(defaultProfile);
localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultProfile)) localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultProfile));
return true return true;
} catch (error) { } catch (error) {
console.error('Error resetting user profile:', error) console.error("Error resetting user profile:", error);
return false return false;
} }
}, []) }, []);
// Get effective OS (selectedOS or detectedOS) // Get effective OS (selectedOS or detectedOS)
const getEffectiveOS = useCallback((): string => { const getEffectiveOS = useCallback((): string => {
return profile.selectedOS || profile.detectedOS || 'ubuntu' return profile.selectedOS || profile.detectedOS || "ubuntu";
}, [profile]) }, [profile]);
// Check if profile is complete enough for recommendations // Check if profile is complete enough for recommendations
const isProfileComplete = useCallback((): boolean => { const isProfileComplete = useCallback((): boolean => {
return profile.categories.length > 0 && getEffectiveOS() !== 'unknown' return profile.categories.length > 0 && getEffectiveOS() !== "unknown";
}, [profile, getEffectiveOS]) }, [profile, getEffectiveOS]);
return { return {
profile, profile,
@@ -174,6 +190,6 @@ export function useRecommendationProfile() {
getEffectiveOS, getEffectiveOS,
isProfileComplete, isProfileComplete,
detectedOS: profile.detectedOS, detectedOS: profile.detectedOS,
hasCompletedOnboarding: profile.hasCompletedOnboarding hasCompletedOnboarding: profile.hasCompletedOnboarding,
} };
} }
+77 -51
View File
@@ -1,101 +1,127 @@
import { Platform, Package, FilterOptions } from '@/types' import { Platform, Package, FilterOptions } from "@/types";
import { RecommendationRequest, RecommendationResponse } from '@/types/recommendations' import {
RecommendationRequest,
RecommendationResponse,
} from "@/types/recommendations";
const API_BASE_URL = (process.env.NEXT_PUBLIC_API_URL && process.env.NEXT_PUBLIC_API_URL.trim() !== '') const API_BASE_URL =
? process.env.NEXT_PUBLIC_API_URL.replace(/\/$/, '') process.env.NEXT_PUBLIC_API_URL &&
: '/api' process.env.NEXT_PUBLIC_API_URL.trim() !== ""
? process.env.NEXT_PUBLIC_API_URL.replace(/\/$/, "")
: "/api";
class ApiClient { class ApiClient {
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> { private async request<T>(
const url = `${API_BASE_URL}${endpoint}` endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${API_BASE_URL}${endpoint}`;
const response = await fetch(url, { const response = await fetch(url, {
headers: { headers: {
'Content-Type': 'application/json', "Content-Type": "application/json",
...options.headers, ...options.headers,
}, },
...options, ...options,
}) });
if (!response.ok) { if (!response.ok) {
throw new Error(`API request failed: ${response.statusText}`) throw new Error(`API request failed: ${response.statusText}`);
} }
return response.json() return response.json();
} }
// Platform operations // Platform operations
async getPlatforms(): Promise<Platform[]> { async getPlatforms(): Promise<Platform[]> {
return this.request<Platform[]>('/platforms') return this.request<Platform[]>("/platforms");
} }
async getPlatform(id: string): Promise<Platform | null> { async getPlatform(id: string): Promise<Platform | null> {
try { try {
return await this.request<Platform>(`/platforms/${id}`) return await this.request<Platform>(`/platforms/${id}`);
} catch (error) { } catch (error) {
return null return null;
} }
} }
// Package operations // Package operations
async getPackages(filters: FilterOptions = {}): Promise<{ packages: Package[], total: number }> { async getPackages(
const params = new URLSearchParams() filters: FilterOptions = {}
): Promise<{ packages: Package[]; total: number }> {
const params = new URLSearchParams();
if (filters.platform_id) params.append('platform_id', filters.platform_id) if (filters.platform_id) params.append("platform_id", filters.platform_id);
if (filters.category_id) params.append('category_id', filters.category_id.toString()) if (filters.category_id)
if (filters.type) params.append('type', filters.type) params.append("category_id", filters.category_id.toString());
if (filters.repository) params.append('repository', filters.repository) if (filters.type) params.append("type", filters.type);
if (filters.search) params.append('search', filters.search) if (filters.repository) params.append("repository", filters.repository);
if (filters.limit) params.append('limit', filters.limit.toString()) if (filters.search) params.append("search", filters.search);
if (filters.offset) params.append('offset', filters.offset.toString()) if (filters.limit) params.append("limit", filters.limit.toString());
if (filters.sort_by) params.append('sort_by', filters.sort_by) if (filters.offset) params.append("offset", filters.offset.toString());
if (filters.sort_order) params.append('sort_order', filters.sort_order) if (filters.sort_by) params.append("sort_by", filters.sort_by);
if (filters.sort_order) params.append("sort_order", filters.sort_order);
const query = params.toString() ? `?${params.toString()}` : '' const query = params.toString() ? `?${params.toString()}` : "";
return this.request<{ packages: Package[], total: number }>(`/packages${query}`) return this.request<{ packages: Package[]; total: number }>(
`/packages${query}`
);
} }
async getPackage(id: string): Promise<Package | null> { async getPackage(id: string): Promise<Package | null> {
try { try {
return await this.request<Package>(`/packages/${id}`) return await this.request<Package>(`/packages/${id}`);
} catch (error) { } catch (error) {
return null return null;
} }
} }
// Sync operations // Sync operations
async syncDebianPackages(): Promise<{ message: string, timestamp: string }> { async syncDebianPackages(): Promise<{ message: string; timestamp: string }> {
return this.request<{ message: string, timestamp: string }>('/sync', { return this.request<{ message: string; timestamp: string }>("/sync", {
method: 'POST', method: "POST",
body: JSON.stringify({ source: 'debian-official' }), body: JSON.stringify({ source: "debian-official" }),
}) });
} }
async syncUbuntuPackages(): Promise<{ message: string, timestamp: string }> { async syncUbuntuPackages(): Promise<{ message: string; timestamp: string }> {
return this.request<{ message: string, timestamp: string }>('/sync', { return this.request<{ message: string; timestamp: string }>("/sync", {
method: 'POST', method: "POST",
body: JSON.stringify({ source: 'ubuntu-official' }), body: JSON.stringify({ source: "ubuntu-official" }),
}) });
} }
async syncAllDebianPackages(): Promise<{ message: string, timestamp: string }> { async syncAllDebianPackages(): Promise<{
return this.request<{ message: string, timestamp: string }>('/sync', { message: string;
method: 'POST', timestamp: string;
body: JSON.stringify({ source: 'all-official' }), }> {
}) return this.request<{ message: string; timestamp: string }>("/sync", {
method: "POST",
body: JSON.stringify({ source: "all-official" }),
});
} }
async getSyncStatus(): Promise<{ status: string, last_sync: string | null, platforms: string[] }> { async getSyncStatus(): Promise<{
return this.request<{ status: string, last_sync: string | null, platforms: string[] }>('/sync') status: string;
last_sync: string | null;
platforms: string[];
}> {
return this.request<{
status: string;
last_sync: string | null;
platforms: string[];
}>("/sync");
} }
// Recommendation operations // Recommendation operations
async getRecommendations(request: RecommendationRequest): Promise<RecommendationResponse> { async getRecommendations(
return this.request<RecommendationResponse>('/recommendations', { request: RecommendationRequest
method: 'POST', ): Promise<RecommendationResponse> {
return this.request<RecommendationResponse>("/recommendations", {
method: "POST",
body: JSON.stringify(request), body: JSON.stringify(request),
}) });
} }
} }
export const apiClient = new ApiClient() export const apiClient = new ApiClient();
+101 -91
View File
@@ -1,16 +1,16 @@
import { PackageService } from './packageService' import { PackageService } from "./packageService";
import { Package } from '@/models/Package' import { Package } from "@/models/Package";
import { import {
RecommendationRequest, RecommendationRequest,
RecommendedPackage, RecommendedPackage,
UserCategory, UserCategory,
ExperienceLevel ExperienceLevel,
} from '@/types/recommendations' } from "@/types/recommendations";
import { import {
getPresetPackageNames, getPresetPackageNames,
getPresetPriority, getPresetPriority,
getRecommendationReason getRecommendationReason,
} from '@/data/recommendationPresets' } from "@/data/recommendationPresets";
/** /**
* Recommendation scoring weights * Recommendation scoring weights
@@ -19,8 +19,8 @@ const SCORING_WEIGHTS = {
CATEGORY_MATCH: 0.4, CATEGORY_MATCH: 0.4,
POPULARITY: 0.3, POPULARITY: 0.3,
OS_COMPATIBILITY: 0.2, OS_COMPATIBILITY: 0.2,
PRESET_BOOST: 0.1 PRESET_BOOST: 0.1,
} };
export class RecommendationService { export class RecommendationService {
/** /**
@@ -29,40 +29,48 @@ export class RecommendationService {
static async generateRecommendations( static async generateRecommendations(
request: RecommendationRequest request: RecommendationRequest
): Promise<RecommendedPackage[]> { ): Promise<RecommendedPackage[]> {
const { platform_id, categories, experienceLevel, limit = 20 } = request const { platform_id, categories, experienceLevel, limit = 20 } = request;
// Step 1: Get preset package names for the user's categories and platform // Step 1: Get preset package names for the user's categories and platform
const presetPackageNames = getPresetPackageNames(categories, platform_id) const presetPackageNames = getPresetPackageNames(categories, platform_id);
// Step 2: Fetch packages from database // Step 2: Fetch packages from database
// First, get preset packages // First, get preset packages
const presetPackages = await this.fetchPresetPackages( const presetPackages = await this.fetchPresetPackages(
presetPackageNames, presetPackageNames,
platform_id platform_id
) );
// Then, get additional packages from categories // Then, get additional packages from categories
const categoryPackages = await this.fetchCategoryPackages( const categoryPackages = await this.fetchCategoryPackages(
categories, categories,
platform_id, platform_id,
limit * 2 // Fetch more to ensure we have enough after filtering limit * 2 // Fetch more to ensure we have enough after filtering
) );
// Step 3: Combine and deduplicate // Step 3: Combine and deduplicate
const allPackages = this.deduplicatePackages([ const allPackages = this.deduplicatePackages([
...presetPackages, ...presetPackages,
...categoryPackages ...categoryPackages,
]) ]);
// Step 4: Score and rank packages // Step 4: Score and rank packages
const scoredPackages = allPackages.map(pkg => const scoredPackages = allPackages.map((pkg) =>
this.scorePackage(pkg, categories, platform_id, presetPackageNames, experienceLevel) this.scorePackage(
pkg,
categories,
platform_id,
presetPackageNames,
experienceLevel
) )
);
// Step 5: Sort by score and limit results // Step 5: Sort by score and limit results
scoredPackages.sort((a, b) => b.recommendationScore - a.recommendationScore) scoredPackages.sort(
(a, b) => b.recommendationScore - a.recommendationScore
);
return scoredPackages.slice(0, limit) return scoredPackages.slice(0, limit);
} }
/** /**
@@ -73,32 +81,32 @@ export class RecommendationService {
platformId: string platformId: string
): Promise<Package[]> { ): Promise<Package[]> {
if (packageNames.length === 0) { if (packageNames.length === 0) {
return [] return [];
} }
try { try {
// Fetch packages by exact name match // Fetch packages by exact name match
const packages: Package[] = [] const packages: Package[] = [];
for (const name of packageNames) { for (const name of packageNames) {
const result = await PackageService.getMany({ const result = await PackageService.getMany({
platform_id: platformId, platform_id: platformId,
search: name, search: name,
limit: 1, limit: 1,
sort_by: 'popularity_score', sort_by: "popularity_score",
sort_order: 'desc' sort_order: "desc",
}) });
// Only add if exact match // Only add if exact match
if (result.packages.length > 0 && result.packages[0].name === name) { if (result.packages.length > 0 && result.packages[0].name === name) {
packages.push(result.packages[0]) packages.push(result.packages[0]);
} }
} }
return packages return packages;
} catch (error) { } catch (error) {
console.error('Error fetching preset packages:', error) console.error("Error fetching preset packages:", error);
return [] return [];
} }
} }
@@ -113,20 +121,20 @@ export class RecommendationService {
try { try {
// Map user categories to database categories // Map user categories to database categories
const categoryMap: Record<UserCategory, string[]> = { const categoryMap: Record<UserCategory, string[]> = {
'development': ['Development', 'Internet'], development: ["Development", "Internet"],
'design': ['Graphics', 'Multimedia'], design: ["Graphics", "Multimedia"],
'multimedia': ['Multimedia', 'Graphics'], multimedia: ["Multimedia", "Graphics"],
'system-tools': ['System', 'Utilities'], "system-tools": ["System", "Utilities"],
'gaming': ['Games'], gaming: ["Games"],
'productivity': ['Office', 'Utilities'], productivity: ["Office", "Utilities"],
'education': ['Science', 'Education'] education: ["Science", "Education"],
} };
// Get all matching packages // Get all matching packages
const packages: Package[] = [] const packages: Package[] = [];
for (const category of categories) { for (const category of categories) {
const dbCategories = categoryMap[category] || [] const dbCategories = categoryMap[category] || [];
// Note: Since we don't have category filtering in current API, // Note: Since we don't have category filtering in current API,
// we'll fetch by popularity and filter client-side // we'll fetch by popularity and filter client-side
@@ -134,17 +142,17 @@ export class RecommendationService {
const result = await PackageService.getMany({ const result = await PackageService.getMany({
platform_id: platformId, platform_id: platformId,
limit: Math.ceil(limit / categories.length), limit: Math.ceil(limit / categories.length),
sort_by: 'popularity_score', sort_by: "popularity_score",
sort_order: 'desc' sort_order: "desc",
}) });
packages.push(...result.packages) packages.push(...result.packages);
} }
return packages return packages;
} catch (error) { } catch (error) {
console.error('Error fetching category packages:', error) console.error("Error fetching category packages:", error);
return [] return [];
} }
} }
@@ -152,14 +160,14 @@ export class RecommendationService {
* Remove duplicate packages (by ID) * Remove duplicate packages (by ID)
*/ */
private static deduplicatePackages(packages: Package[]): Package[] { private static deduplicatePackages(packages: Package[]): Package[] {
const seen = new Set<string>() const seen = new Set<string>();
return packages.filter(pkg => { return packages.filter((pkg) => {
if (seen.has(pkg.id)) { if (seen.has(pkg.id)) {
return false return false;
} }
seen.add(pkg.id) seen.add(pkg.id);
return true return true;
}) });
} }
/** /**
@@ -172,65 +180,67 @@ export class RecommendationService {
presetPackageNames: string[], presetPackageNames: string[],
experienceLevel?: ExperienceLevel experienceLevel?: ExperienceLevel
): RecommendedPackage { ): RecommendedPackage {
let score = 0 let score = 0;
let reason = '' let reason = "";
const isPresetMatch = presetPackageNames.includes(pkg.name) const isPresetMatch = presetPackageNames.includes(pkg.name);
// 1. Category Match Score (40%) // 1. Category Match Score (40%)
// For preset packages, this is always high // For preset packages, this is always high
const categoryScore = isPresetMatch ? 1.0 : 0.5 const categoryScore = isPresetMatch ? 1.0 : 0.5;
score += categoryScore * SCORING_WEIGHTS.CATEGORY_MATCH score += categoryScore * SCORING_WEIGHTS.CATEGORY_MATCH;
// 2. Popularity Score (30%) // 2. Popularity Score (30%)
// Normalize popularity_score (0-100) to 0-1 // Normalize popularity_score (0-100) to 0-1
const popularityScore = (pkg.popularity_score || 0) / 100 const popularityScore = (pkg.popularity_score || 0) / 100;
score += popularityScore * SCORING_WEIGHTS.POPULARITY score += popularityScore * SCORING_WEIGHTS.POPULARITY;
// 3. OS Compatibility Score (20%) // 3. OS Compatibility Score (20%)
// All packages from DB should be compatible, so this is always 1.0 // All packages from DB should be compatible, so this is always 1.0
const osScore = 1.0 const osScore = 1.0;
score += osScore * SCORING_WEIGHTS.OS_COMPATIBILITY score += osScore * SCORING_WEIGHTS.OS_COMPATIBILITY;
// 4. Preset Boost (10%) // 4. Preset Boost (10%)
// Extra boost for preset packages based on priority // Extra boost for preset packages based on priority
let presetBoost = 0 let presetBoost = 0;
if (isPresetMatch) { if (isPresetMatch) {
const priority = getPresetPriority(pkg.name, categories, platformId) const priority = getPresetPriority(pkg.name, categories, platformId);
if (priority !== null) { if (priority !== null) {
presetBoost = priority / 10 // Normalize 1-10 to 0.1-1.0 presetBoost = priority / 10; // Normalize 1-10 to 0.1-1.0
// Get recommendation reason from preset // Get recommendation reason from preset
const presetReason = getRecommendationReason(pkg.name, categories) const presetReason = getRecommendationReason(pkg.name, categories);
if (presetReason) { if (presetReason) {
reason = presetReason reason = presetReason;
} }
} }
} }
score += presetBoost * SCORING_WEIGHTS.PRESET_BOOST score += presetBoost * SCORING_WEIGHTS.PRESET_BOOST;
// Default reason if not from preset // Default reason if not from preset
if (!reason) { if (!reason) {
if (pkg.popularity_score && pkg.popularity_score > 70) { if (pkg.popularity_score && pkg.popularity_score > 70) {
reason = 'Popular choice in the community' reason = "Popular choice in the community";
} else { } else {
reason = 'Recommended for your selected categories' reason = "Recommended for your selected categories";
} }
} }
// Normalize final score to 0-100 // Normalize final score to 0-100
const finalScore = Math.round(score * 100) const finalScore = Math.round(score * 100);
return { return {
id: pkg.id, id: pkg.id,
name: pkg.name, name: pkg.name,
description: pkg.description || 'No description available', description: pkg.description || "No description available",
version: pkg.version || 'latest', version: pkg.version || "latest",
category: typeof pkg.category === 'string' ? pkg.category : pkg.category?.name, category:
license: typeof pkg.license === 'string' ? pkg.license : pkg.license?.name, typeof pkg.category === "string" ? pkg.category : pkg.category?.name,
type: pkg.type || 'cli', license:
typeof pkg.license === "string" ? pkg.license : pkg.license?.name,
type: pkg.type || "cli",
platform: pkg.platform, platform: pkg.platform,
platform_id: pkg.platform_id, platform_id: pkg.platform_id,
repository: pkg.repository || 'official', repository: pkg.repository || "official",
download_url: pkg.download_url, download_url: pkg.download_url,
lastUpdated: pkg.last_updated ? pkg.last_updated.toString() : undefined, lastUpdated: pkg.last_updated ? pkg.last_updated.toString() : undefined,
downloads: pkg.downloads_count, downloads: pkg.downloads_count,
@@ -239,8 +249,8 @@ export class RecommendationService {
tags: pkg.tags, tags: pkg.tags,
recommendationScore: finalScore, recommendationScore: finalScore,
recommendationReason: reason, recommendationReason: reason,
presetMatch: isPresetMatch presetMatch: isPresetMatch,
} };
} }
/** /**
@@ -253,8 +263,8 @@ export class RecommendationService {
return this.generateRecommendations({ return this.generateRecommendations({
platform_id: platformId, platform_id: platformId,
categories: [primaryCategory], categories: [primaryCategory],
limit: 5 limit: 5,
}) });
} }
/** /**
@@ -265,30 +275,30 @@ export class RecommendationService {
categories: UserCategory[], categories: UserCategory[],
totalLimit: number = 20 totalLimit: number = 20
): Promise<RecommendedPackage[]> { ): Promise<RecommendedPackage[]> {
const perCategory = Math.ceil(totalLimit / categories.length) const perCategory = Math.ceil(totalLimit / categories.length);
const allRecommendations: RecommendedPackage[] = [] const allRecommendations: RecommendedPackage[] = [];
for (const category of categories) { for (const category of categories) {
const recommendations = await this.generateRecommendations({ const recommendations = await this.generateRecommendations({
platform_id: platformId, platform_id: platformId,
categories: [category], categories: [category],
limit: perCategory limit: perCategory,
}) });
allRecommendations.push(...recommendations) allRecommendations.push(...recommendations);
} }
// Deduplicate by ID and re-sort // Deduplicate by ID and re-sort
const seen = new Set<string>() const seen = new Set<string>();
const deduplicated = allRecommendations.filter(pkg => { const deduplicated = allRecommendations.filter((pkg) => {
if (seen.has(pkg.id)) { if (seen.has(pkg.id)) {
return false return false;
} }
seen.add(pkg.id) seen.add(pkg.id);
return true return true;
}) });
deduplicated.sort((a, b) => b.recommendationScore - a.recommendationScore) deduplicated.sort((a, b) => b.recommendationScore - a.recommendationScore);
return deduplicated.slice(0, totalLimit) return deduplicated.slice(0, totalLimit);
} }
} }
+53 -53
View File
@@ -1,99 +1,99 @@
import { Package, Platform } from './index' import { Package, Platform } from "./index";
/** /**
* User category types for package recommendations * User category types for package recommendations
*/ */
export type UserCategory = export type UserCategory =
| 'development' | "development"
| 'design' | "design"
| 'multimedia' | "multimedia"
| 'system-tools' | "system-tools"
| 'gaming' | "gaming"
| 'productivity' | "productivity"
| 'education' | "education";
/** /**
* User experience level * User experience level
*/ */
export type ExperienceLevel = 'beginner' | 'intermediate' | 'advanced' export type ExperienceLevel = "beginner" | "intermediate" | "advanced";
/** /**
* User profile stored in localStorage * User profile stored in localStorage
*/ */
export interface UserProfile { export interface UserProfile {
categories: UserCategory[] categories: UserCategory[];
detectedOS?: string detectedOS?: string;
selectedOS?: string // Manual override selectedOS?: string; // Manual override
experienceLevel?: ExperienceLevel experienceLevel?: ExperienceLevel;
hasCompletedOnboarding: boolean hasCompletedOnboarding: boolean;
createdAt: string createdAt: string;
lastUpdated: string lastUpdated: string;
} }
/** /**
* Request payload for recommendation API * Request payload for recommendation API
*/ */
export interface RecommendationRequest { export interface RecommendationRequest {
platform_id: string platform_id: string;
categories: UserCategory[] categories: UserCategory[];
experienceLevel?: ExperienceLevel experienceLevel?: ExperienceLevel;
limit?: number limit?: number;
} }
/** /**
* Recommended package with score * Recommended package with score
*/ */
export interface RecommendedPackage { export interface RecommendedPackage {
id: string id: string;
name: string name: string;
description: string description: string;
version: string version: string;
category?: string category?: string;
license?: string license?: string;
type: 'gui' | 'cli' type: "gui" | "cli";
platform?: string | any platform?: string | any;
platform_id?: string platform_id?: string;
repository: 'official' | 'third-party' | 'aur' repository: "official" | "third-party" | "aur";
download_url?: string download_url?: string;
lastUpdated?: string lastUpdated?: string;
downloads?: number downloads?: number;
popularity?: number popularity?: number;
popularity_score?: number popularity_score?: number;
tags?: string[] tags?: string[];
recommendationScore: number recommendationScore: number;
recommendationReason: string recommendationReason: string;
presetMatch?: boolean presetMatch?: boolean;
} }
/** /**
* Preset package configuration * Preset package configuration
*/ */
export interface PackagePreset { export interface PackagePreset {
packageName: string packageName: string;
platforms: string[] // ['windows', 'macos', 'ubuntu', 'arch', 'fedora'] platforms: string[]; // ['windows', 'macos', 'ubuntu', 'arch', 'fedora']
priority: number // 1-10, higher = more important priority: number; // 1-10, higher = more important
reason: string // Why this package is recommended reason: string; // Why this package is recommended
experienceLevel?: ExperienceLevel[] // Target experience levels experienceLevel?: ExperienceLevel[]; // Target experience levels
} }
/** /**
* Category preset configuration * Category preset configuration
*/ */
export interface CategoryPreset { export interface CategoryPreset {
category: UserCategory category: UserCategory;
packages: PackagePreset[] packages: PackagePreset[];
description: string description: string;
icon: string icon: string;
} }
/** /**
* Recommendation response * Recommendation response
*/ */
export interface RecommendationResponse { export interface RecommendationResponse {
recommendations: RecommendedPackage[] recommendations: RecommendedPackage[];
total: number total: number;
userProfile: { userProfile: {
categories: UserCategory[] categories: UserCategory[];
platform: string platform: string;
} };
} }