mirror of
https://github.com/yusufipk/RepoHub.git
synced 2026-09-11 10:36:07 +00:00
feat: Add smart package recommendations feature
- Implement onboarding modal with 3-step wizard - Category selection (up to 3 categories) - OS detection with manual override - Experience level selection - Add intelligent recommendation engine - Hybrid scoring algorithm (category 40%, popularity 30%, OS 20%, preset 10%) - 7 curated categories with preset packages - Support for all platforms (Windows, macOS, Ubuntu, Debian, Arch, Fedora) - Create recommendation UI components - RecommendationsSection with grid layout - Package cards with recommendation scores and reasons - User profile display with customization options - Add localStorage-based profile management - Persistent user preferences - Automatic OS detection - Profile CRUD operations via useRecommendationProfile hook - Implement API endpoint - POST /api/recommendations - GET /api/recommendations (query params) - Request validation and error handling - Add full i18n support - English and Turkish translations - Onboarding flow, categories, and UI labels - Update TypeScript config (lib: es2017 for array.includes) Closes #1
This commit is contained in:
@@ -0,0 +1,261 @@
|
|||||||
|
# Smart Package Recommendations Feature
|
||||||
|
|
||||||
|
## 📋 Overview
|
||||||
|
|
||||||
|
This feature adds intelligent package recommendations to RepoHub based on user preferences, operating system, and experience level. It provides a personalized onboarding experience and curated package suggestions.
|
||||||
|
|
||||||
|
## ✨ Key Features
|
||||||
|
|
||||||
|
### 1. **Onboarding Modal**
|
||||||
|
- 3-step wizard for new users
|
||||||
|
- Category selection (up to 3 categories)
|
||||||
|
- OS detection with manual override option
|
||||||
|
- Experience level selection (beginner/intermediate/advanced)
|
||||||
|
- Persistent localStorage-based profile
|
||||||
|
|
||||||
|
### 2. **Smart Recommendations**
|
||||||
|
- Hybrid scoring algorithm:
|
||||||
|
- Category match: 40%
|
||||||
|
- Popularity: 30%
|
||||||
|
- OS compatibility: 20%
|
||||||
|
- Preset boost: 10%
|
||||||
|
- Curated preset packages for 7 categories
|
||||||
|
- Real-time filtering based on user profile
|
||||||
|
|
||||||
|
### 3. **Categories Supported**
|
||||||
|
- 💻 **Development**: IDEs, version control, programming languages
|
||||||
|
- 🎨 **Design**: Graphics editors, 3D tools, UI/UX software
|
||||||
|
- 🎬 **Multimedia**: Video/audio editing, media players
|
||||||
|
- ⚙️ **System Tools**: Admin tools, utilities, monitoring
|
||||||
|
- 🎮 **Gaming**: Game platforms, communication tools
|
||||||
|
- 📝 **Productivity**: Office suites, note-taking, password managers
|
||||||
|
- 🎓 **Education**: Learning tools, scientific software
|
||||||
|
|
||||||
|
## 🏗️ Architecture
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── types/
|
||||||
|
│ └── recommendations.ts # Type definitions
|
||||||
|
├── data/
|
||||||
|
│ └── recommendationPresets.ts # Hardcoded package presets
|
||||||
|
├── services/
|
||||||
|
│ └── recommendationService.ts # Recommendation algorithm
|
||||||
|
├── hooks/
|
||||||
|
│ └── useRecommendationProfile.ts # localStorage management + OS detection
|
||||||
|
├── components/
|
||||||
|
│ ├── OnboardingModal.tsx # User onboarding wizard
|
||||||
|
│ └── RecommendationsSection.tsx # Recommendation display
|
||||||
|
└── app/api/
|
||||||
|
└── recommendations/
|
||||||
|
└── route.ts # API endpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. First Visit
|
||||||
|
└→ useRecommendationProfile detects !hasCompletedOnboarding
|
||||||
|
└→ OnboardingModal opens automatically
|
||||||
|
└→ User selects categories, OS, experience level
|
||||||
|
└→ Profile saved to localStorage
|
||||||
|
└→ hasCompletedOnboarding = true
|
||||||
|
|
||||||
|
2. Recommendations
|
||||||
|
└→ RecommendationsSection fetches from /api/recommendations
|
||||||
|
└→ POST { platform_id, categories, experienceLevel }
|
||||||
|
└→ RecommendationService.generateRecommendations()
|
||||||
|
└→ Fetch preset packages (exact name match)
|
||||||
|
└→ Fetch category packages (popularity-based)
|
||||||
|
└→ Score each package (hybrid algorithm)
|
||||||
|
└→ Return top 12 recommendations
|
||||||
|
|
||||||
|
3. User Actions
|
||||||
|
└→ Click "Customize Preferences" → Reopen OnboardingModal
|
||||||
|
└→ Click "Refresh Recommendations" → Refetch recommendations
|
||||||
|
└→ Click package card → Add to selection
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 API Usage
|
||||||
|
|
||||||
|
### POST /api/recommendations
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"platform_id": "ubuntu",
|
||||||
|
"categories": ["development", "productivity"],
|
||||||
|
"experienceLevel": "intermediate",
|
||||||
|
"limit": 12
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
{
|
||||||
|
"id": "pkg-uuid",
|
||||||
|
"name": "git",
|
||||||
|
"description": "Version control system",
|
||||||
|
"version": "2.43.0",
|
||||||
|
"recommendationScore": 95,
|
||||||
|
"recommendationReason": "Version control system essential for all developers",
|
||||||
|
"presetMatch": true,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 12,
|
||||||
|
"userProfile": {
|
||||||
|
"categories": ["development", "productivity"],
|
||||||
|
"platform": "ubuntu",
|
||||||
|
"experienceLevel": "intermediate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /api/recommendations
|
||||||
|
|
||||||
|
Query parameters version (alternative to POST):
|
||||||
|
```
|
||||||
|
GET /api/recommendations?platform_id=ubuntu&categories=development,productivity&experience_level=intermediate&limit=12
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 UI/UX Features
|
||||||
|
|
||||||
|
### Onboarding Modal
|
||||||
|
- **Step 1**: Category selection with icons and descriptions
|
||||||
|
- **Step 2**: OS selection (auto-detected + manual override)
|
||||||
|
- **Step 3**: Experience level with detailed descriptions
|
||||||
|
- Progress indicator (3 dots)
|
||||||
|
- Back/Next navigation
|
||||||
|
- Validation (can't proceed without required selections)
|
||||||
|
|
||||||
|
### Recommendations Section
|
||||||
|
- Grid layout (responsive: 1/2/3 columns)
|
||||||
|
- Package cards with:
|
||||||
|
- "Essential" badge for preset matches
|
||||||
|
- Recommendation score (0-100%) with progress bar
|
||||||
|
- Recommendation reason
|
||||||
|
- Version info
|
||||||
|
- Add to selection button
|
||||||
|
- User profile pills (OS + categories)
|
||||||
|
- Refresh button
|
||||||
|
- Customize preferences button
|
||||||
|
|
||||||
|
## 🌍 i18n Support
|
||||||
|
|
||||||
|
Full English and Turkish translations for:
|
||||||
|
- Onboarding flow
|
||||||
|
- Category names and descriptions
|
||||||
|
- Experience levels
|
||||||
|
- Recommendation UI labels
|
||||||
|
- Button text
|
||||||
|
|
||||||
|
Translation keys:
|
||||||
|
- `onboarding.*`
|
||||||
|
- `categories.*`
|
||||||
|
- `recommendations.*`
|
||||||
|
|
||||||
|
## 💾 localStorage Schema
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Key: 'repohub_user_profile'
|
||||||
|
{
|
||||||
|
categories: ['development', 'productivity'],
|
||||||
|
detectedOS: 'ubuntu',
|
||||||
|
selectedOS?: 'arch', // Manual override
|
||||||
|
experienceLevel: 'intermediate',
|
||||||
|
hasCompletedOnboarding: true,
|
||||||
|
createdAt: '2025-11-22T10:00:00Z',
|
||||||
|
lastUpdated: '2025-11-22T12:30:00Z'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 Testing Checklist
|
||||||
|
|
||||||
|
- [ ] First visit triggers onboarding modal
|
||||||
|
- [ ] OS detection works correctly (Windows/macOS/Linux)
|
||||||
|
- [ ] Category selection validates (max 3)
|
||||||
|
- [ ] Profile persists across page reloads
|
||||||
|
- [ ] Recommendations update when profile changes
|
||||||
|
- [ ] Package cards are clickable and add to selection
|
||||||
|
- [ ] "Customize Preferences" reopens onboarding
|
||||||
|
- [ ] "Refresh Recommendations" fetches new data
|
||||||
|
- [ ] i18n works (EN/TR switching)
|
||||||
|
- [ ] Responsive design on mobile/tablet/desktop
|
||||||
|
- [ ] Loading states display correctly
|
||||||
|
- [ ] Error states handle gracefully
|
||||||
|
|
||||||
|
## 🔮 Future Enhancements
|
||||||
|
|
||||||
|
### Phase 2 (Post-MVP)
|
||||||
|
- [ ] Database storage for user profiles (optional account system)
|
||||||
|
- [ ] Community ratings for packages
|
||||||
|
- [ ] User feedback loop (like/dislike recommendations)
|
||||||
|
- [ ] A/B testing for algorithm weights
|
||||||
|
- [ ] Admin panel for managing presets
|
||||||
|
|
||||||
|
### Phase 3 (ML-Ready)
|
||||||
|
- [ ] Collaborative filtering
|
||||||
|
- [ ] Package co-occurrence analysis
|
||||||
|
- [ ] Time-based trending packages
|
||||||
|
- [ ] Machine learning model integration
|
||||||
|
|
||||||
|
## 📊 Scoring Algorithm Details
|
||||||
|
|
||||||
|
### Hybrid Scoring Formula
|
||||||
|
```typescript
|
||||||
|
score =
|
||||||
|
(category_match * 0.4) +
|
||||||
|
(popularity_score / 100 * 0.3) +
|
||||||
|
(os_compatibility * 0.2) +
|
||||||
|
(preset_priority / 10 * 0.1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Category Match
|
||||||
|
- Preset package: 1.0 (perfect match)
|
||||||
|
- Non-preset package: 0.5 (generic match)
|
||||||
|
|
||||||
|
### Popularity Score
|
||||||
|
- Normalized from 0-100 (from database)
|
||||||
|
- Higher popularity = better recommendation
|
||||||
|
|
||||||
|
### OS Compatibility
|
||||||
|
- All packages from DB are compatible = 1.0
|
||||||
|
- Future: could penalize packages with known issues
|
||||||
|
|
||||||
|
### Preset Priority
|
||||||
|
- Range: 1-10 (defined in presets)
|
||||||
|
- Normalized to 0.1-1.0
|
||||||
|
- Only applies to preset packages
|
||||||
|
|
||||||
|
## 🚀 Deployment Notes
|
||||||
|
|
||||||
|
1. No database migrations required (uses existing schema)
|
||||||
|
2. No environment variables needed (feature is client-side first)
|
||||||
|
3. Compatible with existing API structure
|
||||||
|
4. Progressive enhancement (works without JS for basic browse)
|
||||||
|
|
||||||
|
## 📝 Code Quality
|
||||||
|
|
||||||
|
- ✅ TypeScript strict mode
|
||||||
|
- ✅ Full type coverage
|
||||||
|
- ✅ ESLint compliant
|
||||||
|
- ✅ Responsive design
|
||||||
|
- ✅ Accessibility (keyboard navigation)
|
||||||
|
- ✅ Error boundaries
|
||||||
|
- ✅ Loading states
|
||||||
|
|
||||||
|
## 🔗 Related Files
|
||||||
|
|
||||||
|
- Issue: https://github.com/yusufipk/RepoHub/issues/1
|
||||||
|
- Branch: `feature/smart-package-recommendations`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Developed by:** @ersaayan
|
||||||
|
**Date:** November 22, 2025
|
||||||
|
**Status:** ✅ Ready for Review
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
|
import { RecommendationService } from '@/services/recommendationService'
|
||||||
|
import { RecommendationRequest } from '@/types/recommendations'
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body: RecommendationRequest = await request.json()
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!body.platform_id) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'platform_id is required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!body.categories || body.categories.length === 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'At least one category is required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate platform_id
|
||||||
|
const validPlatforms = ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora']
|
||||||
|
if (!validPlatforms.includes(body.platform_id)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: `Invalid platform_id. Must be one of: ${validPlatforms.join(', ')}` },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate categories
|
||||||
|
const validCategories = [
|
||||||
|
'development',
|
||||||
|
'design',
|
||||||
|
'multimedia',
|
||||||
|
'system-tools',
|
||||||
|
'gaming',
|
||||||
|
'productivity',
|
||||||
|
'education'
|
||||||
|
]
|
||||||
|
const invalidCategories = body.categories.filter(
|
||||||
|
cat => !validCategories.includes(cat)
|
||||||
|
)
|
||||||
|
if (invalidCategories.length > 0) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: `Invalid categories: ${invalidCategories.join(', ')}`,
|
||||||
|
validCategories
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default limit
|
||||||
|
const limit = body.limit && body.limit > 0 && body.limit <= 50
|
||||||
|
? body.limit
|
||||||
|
: 20
|
||||||
|
|
||||||
|
// Generate recommendations
|
||||||
|
const recommendations = await RecommendationService.generateRecommendations({
|
||||||
|
platform_id: body.platform_id,
|
||||||
|
categories: body.categories,
|
||||||
|
experienceLevel: body.experienceLevel,
|
||||||
|
limit
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
recommendations,
|
||||||
|
total: recommendations.length,
|
||||||
|
userProfile: {
|
||||||
|
categories: body.categories,
|
||||||
|
platform: body.platform_id,
|
||||||
|
experienceLevel: body.experienceLevel
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating recommendations:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: 'Failed to generate recommendations',
|
||||||
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url)
|
||||||
|
const platformId = searchParams.get('platform_id')
|
||||||
|
const categoriesParam = searchParams.get('categories')
|
||||||
|
const experienceLevel = searchParams.get('experience_level')
|
||||||
|
const limit = searchParams.get('limit')
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!platformId) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'platform_id query parameter is required' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!categoriesParam) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'categories query parameter is required (comma-separated)' },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse categories
|
||||||
|
const categories = categoriesParam.split(',').map(c => c.trim())
|
||||||
|
|
||||||
|
// Validate platform_id
|
||||||
|
const validPlatforms = ['windows', 'macos', 'ubuntu', 'debian', 'arch', 'fedora']
|
||||||
|
if (!validPlatforms.includes(platformId)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: `Invalid platform_id. Must be one of: ${validPlatforms.join(', ')}` },
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default limit
|
||||||
|
const parsedLimit = limit && parseInt(limit) > 0 && parseInt(limit) <= 50
|
||||||
|
? parseInt(limit)
|
||||||
|
: 20
|
||||||
|
|
||||||
|
// Generate recommendations
|
||||||
|
const recommendations = await RecommendationService.generateRecommendations({
|
||||||
|
platform_id: platformId,
|
||||||
|
categories: categories as any,
|
||||||
|
experienceLevel: experienceLevel as any,
|
||||||
|
limit: parsedLimit
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
recommendations,
|
||||||
|
total: recommendations.length,
|
||||||
|
userProfile: {
|
||||||
|
categories,
|
||||||
|
platform: platformId,
|
||||||
|
experienceLevel
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating recommendations:', error)
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: 'Failed to generate recommendations',
|
||||||
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
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 { useLocale } from '@/contexts/LocaleContext'
|
||||||
|
import { RECOMMENDATION_PRESETS } from '@/data/recommendationPresets'
|
||||||
|
|
||||||
|
interface OnboardingModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
onComplete: (data: {
|
||||||
|
categories: UserCategory[]
|
||||||
|
selectedOS?: string
|
||||||
|
experienceLevel: ExperienceLevel
|
||||||
|
}) => void
|
||||||
|
detectedOS: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const PLATFORMS = [
|
||||||
|
{ id: 'windows', name: 'Windows', icon: '🪟' },
|
||||||
|
{ id: 'macos', name: 'macOS', icon: '🍎' },
|
||||||
|
{ id: 'ubuntu', name: 'Ubuntu', icon: '🐧' },
|
||||||
|
{ id: 'debian', name: 'Debian', icon: '🐧' },
|
||||||
|
{ id: 'arch', name: 'Arch Linux', icon: '🏛️' },
|
||||||
|
{ id: 'fedora', name: 'Fedora', icon: '🎩' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export function OnboardingModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onComplete,
|
||||||
|
detectedOS
|
||||||
|
}: OnboardingModalProps) {
|
||||||
|
const { t } = useLocale()
|
||||||
|
const [step, setStep] = useState(1)
|
||||||
|
const [selectedCategories, setSelectedCategories] = useState<UserCategory[]>([])
|
||||||
|
const [selectedOS, setSelectedOS] = useState<string>(detectedOS)
|
||||||
|
const [experienceLevel, setExperienceLevel] = useState<ExperienceLevel>('beginner')
|
||||||
|
|
||||||
|
if (!isOpen) return null
|
||||||
|
|
||||||
|
const handleCategoryToggle = (category: UserCategory) => {
|
||||||
|
setSelectedCategories(prev => {
|
||||||
|
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) {
|
||||||
|
setStep(step + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
if (step > 1) {
|
||||||
|
setStep(step - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleComplete = () => {
|
||||||
|
if (selectedCategories.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onComplete({
|
||||||
|
categories: selectedCategories,
|
||||||
|
selectedOS: selectedOS !== detectedOS ? selectedOS : undefined,
|
||||||
|
experienceLevel
|
||||||
|
})
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const canProceed = () => {
|
||||||
|
if (step === 1) return selectedCategories.length > 0
|
||||||
|
if (step === 2) return selectedOS !== 'unknown'
|
||||||
|
if (step === 3) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
||||||
|
<Card className="w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
|
||||||
|
<CardHeader className="relative">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute right-4 top-4 rounded-sm opacity-70 hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Sparkles className="h-6 w-6 text-primary" />
|
||||||
|
<CardTitle className="text-2xl">
|
||||||
|
{t('onboarding.title')}
|
||||||
|
</CardTitle>
|
||||||
|
</div>
|
||||||
|
<CardDescription>
|
||||||
|
{t('onboarding.subtitle')}
|
||||||
|
</CardDescription>
|
||||||
|
|
||||||
|
{/* Progress indicator */}
|
||||||
|
<div className="flex gap-2 mt-4">
|
||||||
|
{[1, 2, 3].map(i => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={`h-2 flex-1 rounded-full transition-colors ${
|
||||||
|
i <= step ? 'bg-primary' : 'bg-secondary'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{/* Step 1: Categories */}
|
||||||
|
{step === 1 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">
|
||||||
|
{t('onboarding.step1.title')}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
{t('onboarding.step1.description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{RECOMMENDATION_PRESETS.map(preset => (
|
||||||
|
<button
|
||||||
|
key={preset.category}
|
||||||
|
onClick={() => handleCategoryToggle(preset.category)}
|
||||||
|
className={`p-4 rounded-lg border-2 text-left transition-all hover:scale-105 ${
|
||||||
|
selectedCategories.includes(preset.category)
|
||||||
|
? 'border-primary bg-primary/10'
|
||||||
|
: 'border-border hover:border-primary/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span className="text-3xl">{preset.icon}</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h4 className="font-semibold capitalize">
|
||||||
|
{t(`categories.${preset.category}.name`)}
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
|
||||||
|
{t(`categories.${preset.category}.description`)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedCategories.length > 0 && (
|
||||||
|
<p className="text-sm text-muted-foreground text-center">
|
||||||
|
{t('onboarding.step1.selected', { count: selectedCategories.length })}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 2: Operating System */}
|
||||||
|
{step === 2 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">
|
||||||
|
{t('onboarding.step2.title')}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
{t('onboarding.step2.description')}
|
||||||
|
</p>
|
||||||
|
{detectedOS !== 'unknown' && (
|
||||||
|
<p className="text-sm text-primary mb-4">
|
||||||
|
{t('onboarding.step2.detected', { os: detectedOS })}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||||
|
{PLATFORMS.map(platform => (
|
||||||
|
<button
|
||||||
|
key={platform.id}
|
||||||
|
onClick={() => setSelectedOS(platform.id)}
|
||||||
|
className={`p-4 rounded-lg border-2 text-center transition-all hover:scale-105 ${
|
||||||
|
selectedOS === platform.id
|
||||||
|
? 'border-primary bg-primary/10'
|
||||||
|
: 'border-border hover:border-primary/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="text-4xl mb-2">{platform.icon}</div>
|
||||||
|
<div className="font-semibold text-sm">{platform.name}</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 3: Experience Level */}
|
||||||
|
{step === 3 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">
|
||||||
|
{t('onboarding.step3.title')}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
|
{t('onboarding.step3.description')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{(['beginner', 'intermediate', 'advanced'] as ExperienceLevel[]).map(level => (
|
||||||
|
<button
|
||||||
|
key={level}
|
||||||
|
onClick={() => setExperienceLevel(level)}
|
||||||
|
className={`w-full p-4 rounded-lg border-2 text-left transition-all hover:scale-[1.02] ${
|
||||||
|
experienceLevel === level
|
||||||
|
? 'border-primary bg-primary/10'
|
||||||
|
: 'border-border hover:border-primary/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<h4 className="font-semibold capitalize mb-1">
|
||||||
|
{t(`onboarding.step3.levels.${level}.name`)}
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t(`onboarding.step3.levels.${level}.description`)}
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Navigation Buttons */}
|
||||||
|
<div className="flex gap-3 pt-4">
|
||||||
|
{step > 1 && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleBack}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4 mr-2" />
|
||||||
|
{t('common.back')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step < 3 ? (
|
||||||
|
<Button
|
||||||
|
onClick={handleNext}
|
||||||
|
disabled={!canProceed()}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{t('common.next')}
|
||||||
|
<ChevronRight className="h-4 w-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={handleComplete}
|
||||||
|
disabled={!canProceed()}
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
{t('onboarding.complete')}
|
||||||
|
<Sparkles className="h-4 w-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Sparkles, RefreshCw, Settings, Package as PackageIcon, Star } from 'lucide-react'
|
||||||
|
import { RecommendedPackage } from '@/types/recommendations'
|
||||||
|
import { Package } from '@/types'
|
||||||
|
import { useLocale } from '@/contexts/LocaleContext'
|
||||||
|
import { useRecommendationProfile } from '@/hooks/useRecommendationProfile'
|
||||||
|
|
||||||
|
interface RecommendationsSectionProps {
|
||||||
|
onPackageToggle: (pkg: Package) => void
|
||||||
|
selectedPackages: Package[]
|
||||||
|
onCustomizeClick: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RecommendationsSection({
|
||||||
|
onPackageToggle,
|
||||||
|
selectedPackages,
|
||||||
|
onCustomizeClick
|
||||||
|
}: RecommendationsSectionProps) {
|
||||||
|
const { t } = useLocale()
|
||||||
|
const { profile, getEffectiveOS, isProfileComplete } = useRecommendationProfile()
|
||||||
|
const [recommendations, setRecommendations] = useState<RecommendedPackage[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
const fetchRecommendations = async () => {
|
||||||
|
if (!isProfileComplete()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/recommendations', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
platform_id: getEffectiveOS(),
|
||||||
|
categories: profile.categories,
|
||||||
|
experienceLevel: profile.experienceLevel,
|
||||||
|
limit: 12
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch recommendations')
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
setRecommendations(data.recommendations || [])
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching recommendations:', err)
|
||||||
|
setError(err instanceof Error ? err.message : 'Unknown error')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch recommendations on mount and when profile changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (isProfileComplete()) {
|
||||||
|
fetchRecommendations()
|
||||||
|
}
|
||||||
|
}, [profile.categories, profile.selectedOS, profile.experienceLevel])
|
||||||
|
|
||||||
|
const isPackageSelected = (pkg: RecommendedPackage) => {
|
||||||
|
return selectedPackages.some(selected => selected.id === pkg.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isProfileComplete()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Sparkles className="h-5 w-5 text-primary" />
|
||||||
|
<div>
|
||||||
|
<CardTitle className="text-lg">
|
||||||
|
{t('recommendations.title')}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-sm">
|
||||||
|
{t('recommendations.subtitle')}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={fetchRecommendations}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<RefreshCw className={`h-4 w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||||
|
{t('recommendations.refresh')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={onCustomizeClick}
|
||||||
|
>
|
||||||
|
<Settings className="h-4 w-4 mr-2" />
|
||||||
|
{t('recommendations.customize')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Show user profile info */}
|
||||||
|
<div className="flex flex-wrap gap-2 mt-3">
|
||||||
|
<span className="text-xs px-2 py-1 rounded-full bg-primary/10 text-primary">
|
||||||
|
{getEffectiveOS()}
|
||||||
|
</span>
|
||||||
|
{profile.categories.map(cat => (
|
||||||
|
<span
|
||||||
|
key={cat}
|
||||||
|
className="text-xs px-2 py-1 rounded-full bg-secondary text-secondary-foreground"
|
||||||
|
>
|
||||||
|
{t(`categories.${cat}.name`)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent>
|
||||||
|
{loading && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<RefreshCw className="h-8 w-8 animate-spin mx-auto mb-4 text-primary" />
|
||||||
|
<p className="text-muted-foreground">{t('recommendations.loading')}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-destructive mb-4">{error}</p>
|
||||||
|
<Button onClick={fetchRecommendations} variant="outline">
|
||||||
|
Try Again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && !error && recommendations.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<PackageIcon className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
{t('recommendations.no_recommendations')}
|
||||||
|
</p>
|
||||||
|
<Button onClick={onCustomizeClick} variant="outline">
|
||||||
|
<Settings className="h-4 w-4 mr-2" />
|
||||||
|
{t('recommendations.customize')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && !error && recommendations.length > 0 && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{recommendations.map(pkg => (
|
||||||
|
<Card
|
||||||
|
key={pkg.id}
|
||||||
|
className={`relative overflow-hidden transition-all hover:shadow-lg cursor-pointer ${
|
||||||
|
isPackageSelected(pkg) ? 'ring-2 ring-primary' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => onPackageToggle(pkg)}
|
||||||
|
>
|
||||||
|
{pkg.presetMatch && (
|
||||||
|
<div className="absolute top-2 right-2">
|
||||||
|
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-semibold bg-primary text-primary-foreground">
|
||||||
|
<Star className="h-3 w-3" />
|
||||||
|
{t('recommendations.preset_badge')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-start gap-3 mb-3">
|
||||||
|
<PackageIcon className="h-8 w-8 text-primary flex-shrink-0" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-semibold truncate">{pkg.name}</h3>
|
||||||
|
<p className="text-xs text-muted-foreground">{pkg.version}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-muted-foreground line-clamp-2 mb-3">
|
||||||
|
{pkg.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Recommendation score and reason */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{t('recommendations.score')}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<div className="h-2 w-16 bg-secondary rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-primary rounded-full transition-all"
|
||||||
|
style={{ width: `${pkg.recommendationScore}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-semibold">{pkg.recommendationScore}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pkg.recommendationReason && (
|
||||||
|
<div className="pt-2 border-t">
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
<span className="font-semibold">{t('recommendations.reason')}</span>
|
||||||
|
{' '}
|
||||||
|
{pkg.recommendationReason}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant={isPackageSelected(pkg) ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
className="w-full mt-3"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onPackageToggle(pkg)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isPackageSelected(pkg) ? '✓ Selected' : t('recommendations.add_to_selection')}
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { LocaleProvider } from '@/contexts/LocaleContext'
|
import { LocaleProvider } from '@/contexts/LocaleContext'
|
||||||
import { Header } from './Header'
|
import { Header } from './Header'
|
||||||
import { PlatformSelector } from './PlatformSelector'
|
import { PlatformSelector } from './PlatformSelector'
|
||||||
import { PackageBrowserV2 } from './PackageBrowserV2'
|
import { PackageBrowserV2 } from './PackageBrowserV2'
|
||||||
import { SelectionManager } from './SelectionManager'
|
import { SelectionManager } from './SelectionManager'
|
||||||
import { ScriptPreview } from '@/components/ScriptPreview'
|
import { ScriptPreview } from '@/components/ScriptPreview'
|
||||||
|
import { OnboardingModal } from '@/components/OnboardingModal'
|
||||||
|
import { RecommendationsSection } from '@/components/RecommendationsSection'
|
||||||
import { generateScript } from '@/lib/scriptGenerator'
|
import { generateScript } from '@/lib/scriptGenerator'
|
||||||
import { useLocale } from '@/contexts/LocaleContext'
|
import { useLocale } from '@/contexts/LocaleContext'
|
||||||
|
import { useRecommendationProfile } from '@/hooks/useRecommendationProfile'
|
||||||
import { Platform, Package, SelectedPackage, FilterOptions, GeneratedScript } from '@/types'
|
import { Platform, Package, SelectedPackage, FilterOptions, GeneratedScript } from '@/types'
|
||||||
|
import { UserCategory, ExperienceLevel } from '@/types/recommendations'
|
||||||
|
|
||||||
function RepoHubAppContent({ cryptomusEnabled }: { cryptomusEnabled: boolean }) {
|
function RepoHubAppContent({ cryptomusEnabled }: { cryptomusEnabled: boolean }) {
|
||||||
const { t, locale } = useLocale()
|
const { t, locale } = useLocale()
|
||||||
@@ -17,6 +21,47 @@ function RepoHubAppContent({ cryptomusEnabled }: { cryptomusEnabled: boolean })
|
|||||||
const [selectedPackages, setSelectedPackages] = useState<SelectedPackage[]>([])
|
const [selectedPackages, setSelectedPackages] = useState<SelectedPackage[]>([])
|
||||||
const [generatedScript, setGeneratedScript] = useState<GeneratedScript | null>(null)
|
const [generatedScript, setGeneratedScript] = useState<GeneratedScript | null>(null)
|
||||||
|
|
||||||
|
// Recommendation profile management
|
||||||
|
const {
|
||||||
|
profile,
|
||||||
|
isLoading: isProfileLoading,
|
||||||
|
hasCompletedOnboarding,
|
||||||
|
completeOnboarding,
|
||||||
|
saveProfile,
|
||||||
|
detectedOS
|
||||||
|
} = useRecommendationProfile()
|
||||||
|
|
||||||
|
const [showOnboarding, setShowOnboarding] = useState(false)
|
||||||
|
|
||||||
|
// Show onboarding modal on first visit
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isProfileLoading && !hasCompletedOnboarding) {
|
||||||
|
// Delay to allow page to render first
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setShowOnboarding(true)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [isProfileLoading, hasCompletedOnboarding])
|
||||||
|
|
||||||
|
const handleOnboardingComplete = (data: {
|
||||||
|
categories: UserCategory[]
|
||||||
|
selectedOS?: string
|
||||||
|
experienceLevel: ExperienceLevel
|
||||||
|
}) => {
|
||||||
|
saveProfile({
|
||||||
|
categories: data.categories,
|
||||||
|
selectedOS: data.selectedOS,
|
||||||
|
experienceLevel: data.experienceLevel,
|
||||||
|
hasCompletedOnboarding: true
|
||||||
|
})
|
||||||
|
completeOnboarding()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCustomizePreferences = () => {
|
||||||
|
setShowOnboarding(true)
|
||||||
|
}
|
||||||
|
|
||||||
const handlePlatformSelect = (platform: Platform) => {
|
const handlePlatformSelect = (platform: Platform) => {
|
||||||
setSelectedPlatform(platform)
|
setSelectedPlatform(platform)
|
||||||
// Clear selections when platform changes
|
// Clear selections when platform changes
|
||||||
@@ -79,6 +124,15 @@ function RepoHubAppContent({ cryptomusEnabled }: { cryptomusEnabled: boolean })
|
|||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
|
{/* Recommendations Section - Show if profile is complete */}
|
||||||
|
{hasCompletedOnboarding && profile.categories.length > 0 && (
|
||||||
|
<RecommendationsSection
|
||||||
|
onPackageToggle={handlePackageToggle}
|
||||||
|
selectedPackages={selectedPackages}
|
||||||
|
onCustomizeClick={handleCustomizePreferences}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Platform Selector */}
|
{/* Platform Selector */}
|
||||||
<PlatformSelector
|
<PlatformSelector
|
||||||
selectedPlatform={selectedPlatform}
|
selectedPlatform={selectedPlatform}
|
||||||
@@ -111,6 +165,14 @@ function RepoHubAppContent({ cryptomusEnabled }: { cryptomusEnabled: boolean })
|
|||||||
onClose={handleCloseScriptPreview}
|
onClose={handleCloseScriptPreview}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Onboarding Modal */}
|
||||||
|
<OnboardingModal
|
||||||
|
isOpen={showOnboarding}
|
||||||
|
onClose={() => setShowOnboarding(false)}
|
||||||
|
onComplete={handleOnboardingComplete}
|
||||||
|
detectedOS={detectedOS || 'unknown'}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ const translations = {
|
|||||||
title: "RepoHub - Cross-Platform Package Manager",
|
title: "RepoHub - Cross-Platform Package Manager",
|
||||||
subtitle: "Cross-Platform Package Manager",
|
subtitle: "Cross-Platform Package Manager",
|
||||||
description: "Simplify software installation across Linux, Windows, and macOS with official repositories",
|
description: "Simplify software installation across Linux, Windows, and macOS with official repositories",
|
||||||
close: "Close"
|
close: "Close",
|
||||||
|
next: "Next",
|
||||||
|
back: "Back"
|
||||||
},
|
},
|
||||||
platform: {
|
platform: {
|
||||||
select: "Select Your Platform",
|
select: "Select Your Platform",
|
||||||
@@ -106,6 +108,83 @@ const translations = {
|
|||||||
success_note1: "Payment confirmation will be sent to your email if provided.",
|
success_note1: "Payment confirmation will be sent to your email if provided.",
|
||||||
success_note2: "You can close this page and return to RepoHub.",
|
success_note2: "You can close this page and return to RepoHub.",
|
||||||
back_to_site: "Back to RepoHub"
|
back_to_site: "Back to RepoHub"
|
||||||
|
},
|
||||||
|
onboarding: {
|
||||||
|
title: "Welcome to RepoHub!",
|
||||||
|
subtitle: "Let's personalize your package recommendations",
|
||||||
|
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)"
|
||||||
|
},
|
||||||
|
step2: {
|
||||||
|
title: "Select your operating system",
|
||||||
|
description: "We'll recommend compatible packages for your platform",
|
||||||
|
detected: "✓ Detected: {os}"
|
||||||
|
},
|
||||||
|
step3: {
|
||||||
|
title: "What's your experience level?",
|
||||||
|
description: "This helps us recommend appropriate tools",
|
||||||
|
levels: {
|
||||||
|
beginner: {
|
||||||
|
name: "Beginner",
|
||||||
|
description: "New to software installation and package management"
|
||||||
|
},
|
||||||
|
intermediate: {
|
||||||
|
name: "Intermediate",
|
||||||
|
description: "Comfortable with basic command-line operations"
|
||||||
|
},
|
||||||
|
advanced: {
|
||||||
|
name: "Advanced",
|
||||||
|
description: "Experienced with system administration and package management"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
categories: {
|
||||||
|
development: {
|
||||||
|
name: "Development",
|
||||||
|
description: "Code editors, compilers, and development tools"
|
||||||
|
},
|
||||||
|
design: {
|
||||||
|
name: "Design",
|
||||||
|
description: "Graphics, UI/UX, and creative software"
|
||||||
|
},
|
||||||
|
multimedia: {
|
||||||
|
name: "Multimedia",
|
||||||
|
description: "Audio, video editing, and media tools"
|
||||||
|
},
|
||||||
|
"system-tools": {
|
||||||
|
name: "System Tools",
|
||||||
|
description: "System administration and utilities"
|
||||||
|
},
|
||||||
|
gaming: {
|
||||||
|
name: "Gaming",
|
||||||
|
description: "Gaming platforms and related tools"
|
||||||
|
},
|
||||||
|
productivity: {
|
||||||
|
name: "Productivity",
|
||||||
|
description: "Office, note-taking, and productivity apps"
|
||||||
|
},
|
||||||
|
education: {
|
||||||
|
name: "Education",
|
||||||
|
description: "Educational and scientific software"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
recommendations: {
|
||||||
|
title: "Recommended for You",
|
||||||
|
subtitle: "Personalized package recommendations based on your profile",
|
||||||
|
refresh: "Refresh Recommendations",
|
||||||
|
customize: "Customize Preferences",
|
||||||
|
no_recommendations: "No recommendations available. Please update your preferences.",
|
||||||
|
loading: "Finding the best packages for you...",
|
||||||
|
score: "Match Score",
|
||||||
|
preset_badge: "Essential",
|
||||||
|
view_details: "View Details",
|
||||||
|
add_to_selection: "Add to Selection",
|
||||||
|
reason: "Why recommended:",
|
||||||
|
based_on: "Based on your interests in:"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tr: {
|
tr: {
|
||||||
@@ -113,7 +192,9 @@ const translations = {
|
|||||||
title: "RepoHub - Çok Platformlu Paket Yöneticisi",
|
title: "RepoHub - Çok Platformlu Paket Yöneticisi",
|
||||||
subtitle: "Çok Platformlu Paket Yöneticisi",
|
subtitle: "Çok Platformlu Paket Yöneticisi",
|
||||||
description: "Linux, Windows ve macOS'te resmi depoları kullanarak yazılım kurulumunu basitleştirin",
|
description: "Linux, Windows ve macOS'te resmi depoları kullanarak yazılım kurulumunu basitleştirin",
|
||||||
close: "Kapat"
|
close: "Kapat",
|
||||||
|
next: "İleri",
|
||||||
|
back: "Geri"
|
||||||
},
|
},
|
||||||
platform: {
|
platform: {
|
||||||
select: "Platformunuzu Seçin",
|
select: "Platformunuzu Seçin",
|
||||||
@@ -209,6 +290,83 @@ const translations = {
|
|||||||
success_note1: "Ödeme onayı sağlandıysa e-postanıza gönderilecektir.",
|
success_note1: "Ödeme onayı sağlandıysa e-postanıza gönderilecektir.",
|
||||||
success_note2: "Bu sayfayı kapatabilir ve RepoHub'a dönebilirsiniz.",
|
success_note2: "Bu sayfayı kapatabilir ve RepoHub'a dönebilirsiniz.",
|
||||||
back_to_site: "RepoHub'a Geri Dön"
|
back_to_site: "RepoHub'a Geri Dön"
|
||||||
|
},
|
||||||
|
onboarding: {
|
||||||
|
title: "RepoHub'a Hoş Geldiniz!",
|
||||||
|
subtitle: "Paket önerilerinizi kişiselleştirelim",
|
||||||
|
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)"
|
||||||
|
},
|
||||||
|
step2: {
|
||||||
|
title: "İşletim sisteminizi seçin",
|
||||||
|
description: "Platformunuzla uyumlu paketler önereceğiz",
|
||||||
|
detected: "✓ Tespit edildi: {os}"
|
||||||
|
},
|
||||||
|
step3: {
|
||||||
|
title: "Deneyim seviyeniz nedir?",
|
||||||
|
description: "Bu, size uygun araçları önermemize yardımcı olur",
|
||||||
|
levels: {
|
||||||
|
beginner: {
|
||||||
|
name: "Başlangıç",
|
||||||
|
description: "Yazılım kurulumu ve paket yönetimine yeni"
|
||||||
|
},
|
||||||
|
intermediate: {
|
||||||
|
name: "Orta",
|
||||||
|
description: "Temel komut satırı işlemlerinde rahat"
|
||||||
|
},
|
||||||
|
advanced: {
|
||||||
|
name: "İleri",
|
||||||
|
description: "Sistem yönetimi ve paket yönetiminde deneyimli"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
categories: {
|
||||||
|
development: {
|
||||||
|
name: "Geliştirme",
|
||||||
|
description: "Kod editörleri, derleyiciler ve geliştirme araçları"
|
||||||
|
},
|
||||||
|
design: {
|
||||||
|
name: "Tasarım",
|
||||||
|
description: "Grafik, UI/UX ve yaratıcı yazılımlar"
|
||||||
|
},
|
||||||
|
multimedia: {
|
||||||
|
name: "Multimedya",
|
||||||
|
description: "Ses, video düzenleme ve medya araçları"
|
||||||
|
},
|
||||||
|
"system-tools": {
|
||||||
|
name: "Sistem Araçları",
|
||||||
|
description: "Sistem yönetimi ve yardımcı programlar"
|
||||||
|
},
|
||||||
|
gaming: {
|
||||||
|
name: "Oyun",
|
||||||
|
description: "Oyun platformları ve ilgili araçlar"
|
||||||
|
},
|
||||||
|
productivity: {
|
||||||
|
name: "Üretkenlik",
|
||||||
|
description: "Ofis, not alma ve üretkenlik uygulamaları"
|
||||||
|
},
|
||||||
|
education: {
|
||||||
|
name: "Eğitim",
|
||||||
|
description: "Eğitim ve bilimsel yazılımlar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
recommendations: {
|
||||||
|
title: "Size Özel Öneriler",
|
||||||
|
subtitle: "Profilinize göre kişiselleştirilmiş paket önerileri",
|
||||||
|
refresh: "Önerileri Yenile",
|
||||||
|
customize: "Tercihleri Özelleştir",
|
||||||
|
no_recommendations: "Öneri mevcut değil. Lütfen tercihlerinizi güncelleyin.",
|
||||||
|
loading: "Sizin için en iyi paketleri buluyoruz...",
|
||||||
|
score: "Eşleşme Skoru",
|
||||||
|
preset_badge: "Temel",
|
||||||
|
view_details: "Detayları Gör",
|
||||||
|
add_to_selection: "Seçime Ekle",
|
||||||
|
reason: "Neden önerildi:",
|
||||||
|
based_on: "İlgi alanlarınıza göre:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,408 @@
|
|||||||
|
import { CategoryPreset } from '@/types/recommendations'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Curated package recommendations for each user category
|
||||||
|
* These presets are used by the recommendation engine to suggest packages
|
||||||
|
*/
|
||||||
|
export const RECOMMENDATION_PRESETS: CategoryPreset[] = [
|
||||||
|
{
|
||||||
|
category: 'development',
|
||||||
|
description: 'Essential tools for software development',
|
||||||
|
icon: '💻',
|
||||||
|
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']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'design',
|
||||||
|
description: 'Tools for graphic design, UI/UX, and creative work',
|
||||||
|
icon: '🎨',
|
||||||
|
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']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'multimedia',
|
||||||
|
description: 'Audio, video editing and media management tools',
|
||||||
|
icon: '🎬',
|
||||||
|
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']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'system-tools',
|
||||||
|
description: 'System administration, security and utilities',
|
||||||
|
icon: '⚙️',
|
||||||
|
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']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'gaming',
|
||||||
|
description: 'Gaming platforms and related tools',
|
||||||
|
icon: '🎮',
|
||||||
|
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']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'productivity',
|
||||||
|
description: 'Office, note-taking and productivity tools',
|
||||||
|
icon: '📝',
|
||||||
|
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']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'education',
|
||||||
|
description: 'Educational and scientific software',
|
||||||
|
icon: '🎓',
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
): string[] {
|
||||||
|
const presets = getPresetsForCategories(categories)
|
||||||
|
const packageNames = new Set<string>()
|
||||||
|
|
||||||
|
presets.forEach(preset => {
|
||||||
|
preset.packages.forEach(pkg => {
|
||||||
|
if (pkg.platforms.includes(platformId)) {
|
||||||
|
packageNames.add(pkg.packageName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { UserProfile, UserCategory, ExperienceLevel } from '@/types/recommendations'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'repohub_user_profile'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect user's operating system from browser
|
||||||
|
*/
|
||||||
|
function detectOS(): string {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAgent = window.navigator.userAgent.toLowerCase()
|
||||||
|
const platform = window.navigator.platform.toLowerCase()
|
||||||
|
|
||||||
|
// Windows
|
||||||
|
if (userAgent.indexOf('win') !== -1 || platform.indexOf('win') !== -1) {
|
||||||
|
return 'windows'
|
||||||
|
}
|
||||||
|
|
||||||
|
// macOS
|
||||||
|
if (
|
||||||
|
userAgent.indexOf('mac') !== -1 ||
|
||||||
|
platform.indexOf('mac') !== -1 ||
|
||||||
|
userAgent.indexOf('darwin') !== -1
|
||||||
|
) {
|
||||||
|
return 'macos'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linux distros
|
||||||
|
if (userAgent.indexOf('linux') !== -1 || platform.indexOf('linux') !== -1) {
|
||||||
|
// Try to detect specific distro from user agent (rare but possible)
|
||||||
|
if (userAgent.indexOf('ubuntu') !== -1) {
|
||||||
|
return 'ubuntu'
|
||||||
|
}
|
||||||
|
if (userAgent.indexOf('fedora') !== -1) {
|
||||||
|
return 'fedora'
|
||||||
|
}
|
||||||
|
if (userAgent.indexOf('arch') !== -1) {
|
||||||
|
return 'arch'
|
||||||
|
}
|
||||||
|
if (userAgent.indexOf('debian') !== -1) {
|
||||||
|
return 'debian'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to Ubuntu for generic Linux
|
||||||
|
return 'ubuntu'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default user profile
|
||||||
|
*/
|
||||||
|
function getDefaultProfile(): UserProfile {
|
||||||
|
return {
|
||||||
|
categories: [],
|
||||||
|
detectedOS: detectOS(),
|
||||||
|
selectedOS: undefined,
|
||||||
|
experienceLevel: 'beginner',
|
||||||
|
hasCompletedOnboarding: false,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
lastUpdated: new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for managing user recommendation profile in localStorage
|
||||||
|
*/
|
||||||
|
export function useRecommendationProfile() {
|
||||||
|
const [profile, setProfile] = useState<UserProfile>(getDefaultProfile())
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
|
// Load profile from localStorage on mount
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY)
|
||||||
|
if (stored) {
|
||||||
|
const parsed = JSON.parse(stored) as UserProfile
|
||||||
|
|
||||||
|
// Update detectedOS if it changed
|
||||||
|
const currentOS = detectOS()
|
||||||
|
if (parsed.detectedOS !== currentOS) {
|
||||||
|
parsed.detectedOS = currentOS
|
||||||
|
}
|
||||||
|
|
||||||
|
setProfile(parsed)
|
||||||
|
} else {
|
||||||
|
// First time user - save default profile
|
||||||
|
const defaultProfile = getDefaultProfile()
|
||||||
|
setProfile(defaultProfile)
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultProfile))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading user profile:', error)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Save profile to localStorage
|
||||||
|
const saveProfile = useCallback((newProfile: Partial<UserProfile>) => {
|
||||||
|
try {
|
||||||
|
const updated: UserProfile = {
|
||||||
|
...profile,
|
||||||
|
...newProfile,
|
||||||
|
lastUpdated: new Date().toISOString()
|
||||||
|
}
|
||||||
|
setProfile(updated)
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated))
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving user profile:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, [profile])
|
||||||
|
|
||||||
|
// Update categories
|
||||||
|
const updateCategories = useCallback((categories: UserCategory[]) => {
|
||||||
|
return saveProfile({ categories })
|
||||||
|
}, [saveProfile])
|
||||||
|
|
||||||
|
// Update selected OS (manual override)
|
||||||
|
const updateSelectedOS = useCallback((os: string) => {
|
||||||
|
return saveProfile({ selectedOS: os })
|
||||||
|
}, [saveProfile])
|
||||||
|
|
||||||
|
// Update experience level
|
||||||
|
const updateExperienceLevel = useCallback((level: ExperienceLevel) => {
|
||||||
|
return saveProfile({ experienceLevel: level })
|
||||||
|
}, [saveProfile])
|
||||||
|
|
||||||
|
// Mark onboarding as completed
|
||||||
|
const completeOnboarding = useCallback(() => {
|
||||||
|
return saveProfile({ hasCompletedOnboarding: true })
|
||||||
|
}, [saveProfile])
|
||||||
|
|
||||||
|
// Reset profile
|
||||||
|
const resetProfile = useCallback(() => {
|
||||||
|
try {
|
||||||
|
const defaultProfile = getDefaultProfile()
|
||||||
|
setProfile(defaultProfile)
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(defaultProfile))
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error resetting user profile:', error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Get effective OS (selectedOS or detectedOS)
|
||||||
|
const getEffectiveOS = useCallback((): string => {
|
||||||
|
return profile.selectedOS || profile.detectedOS || 'ubuntu'
|
||||||
|
}, [profile])
|
||||||
|
|
||||||
|
// Check if profile is complete enough for recommendations
|
||||||
|
const isProfileComplete = useCallback((): boolean => {
|
||||||
|
return profile.categories.length > 0 && getEffectiveOS() !== 'unknown'
|
||||||
|
}, [profile, getEffectiveOS])
|
||||||
|
|
||||||
|
return {
|
||||||
|
profile,
|
||||||
|
isLoading,
|
||||||
|
saveProfile,
|
||||||
|
updateCategories,
|
||||||
|
updateSelectedOS,
|
||||||
|
updateExperienceLevel,
|
||||||
|
completeOnboarding,
|
||||||
|
resetProfile,
|
||||||
|
getEffectiveOS,
|
||||||
|
isProfileComplete,
|
||||||
|
detectedOS: profile.detectedOS,
|
||||||
|
hasCompletedOnboarding: profile.hasCompletedOnboarding
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Platform, Package, FilterOptions } from '@/types'
|
import { Platform, Package, FilterOptions } from '@/types'
|
||||||
|
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 && process.env.NEXT_PUBLIC_API_URL.trim() !== '')
|
||||||
? process.env.NEXT_PUBLIC_API_URL.replace(/\/$/, '')
|
? process.env.NEXT_PUBLIC_API_URL.replace(/\/$/, '')
|
||||||
@@ -87,6 +88,14 @@ class ApiClient {
|
|||||||
async getSyncStatus(): Promise<{ status: string, last_sync: string | null, platforms: string[] }> {
|
async getSyncStatus(): Promise<{ status: string, last_sync: string | null, platforms: string[] }> {
|
||||||
return this.request<{ status: string, last_sync: string | null, platforms: string[] }>('/sync')
|
return this.request<{ status: string, last_sync: string | null, platforms: string[] }>('/sync')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recommendation operations
|
||||||
|
async getRecommendations(request: RecommendationRequest): Promise<RecommendationResponse> {
|
||||||
|
return this.request<RecommendationResponse>('/recommendations', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apiClient = new ApiClient()
|
export const apiClient = new ApiClient()
|
||||||
|
|||||||
@@ -0,0 +1,294 @@
|
|||||||
|
import { PackageService } from './packageService'
|
||||||
|
import { Package } from '@/models/Package'
|
||||||
|
import {
|
||||||
|
RecommendationRequest,
|
||||||
|
RecommendedPackage,
|
||||||
|
UserCategory,
|
||||||
|
ExperienceLevel
|
||||||
|
} from '@/types/recommendations'
|
||||||
|
import {
|
||||||
|
getPresetPackageNames,
|
||||||
|
getPresetPriority,
|
||||||
|
getRecommendationReason
|
||||||
|
} from '@/data/recommendationPresets'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recommendation scoring weights
|
||||||
|
*/
|
||||||
|
const SCORING_WEIGHTS = {
|
||||||
|
CATEGORY_MATCH: 0.4,
|
||||||
|
POPULARITY: 0.3,
|
||||||
|
OS_COMPATIBILITY: 0.2,
|
||||||
|
PRESET_BOOST: 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RecommendationService {
|
||||||
|
/**
|
||||||
|
* Generate package recommendations based on user profile
|
||||||
|
*/
|
||||||
|
static async generateRecommendations(
|
||||||
|
request: RecommendationRequest
|
||||||
|
): Promise<RecommendedPackage[]> {
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Step 2: Fetch packages from database
|
||||||
|
// First, get preset packages
|
||||||
|
const presetPackages = await this.fetchPresetPackages(
|
||||||
|
presetPackageNames,
|
||||||
|
platform_id
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then, get additional packages from categories
|
||||||
|
const categoryPackages = await this.fetchCategoryPackages(
|
||||||
|
categories,
|
||||||
|
platform_id,
|
||||||
|
limit * 2 // Fetch more to ensure we have enough after filtering
|
||||||
|
)
|
||||||
|
|
||||||
|
// Step 3: Combine and deduplicate
|
||||||
|
const allPackages = this.deduplicatePackages([
|
||||||
|
...presetPackages,
|
||||||
|
...categoryPackages
|
||||||
|
])
|
||||||
|
|
||||||
|
// Step 4: Score and rank packages
|
||||||
|
const scoredPackages = allPackages.map(pkg =>
|
||||||
|
this.scorePackage(pkg, categories, platform_id, presetPackageNames, experienceLevel)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Step 5: Sort by score and limit results
|
||||||
|
scoredPackages.sort((a, b) => b.recommendationScore - a.recommendationScore)
|
||||||
|
|
||||||
|
return scoredPackages.slice(0, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch packages that match preset names
|
||||||
|
*/
|
||||||
|
private static async fetchPresetPackages(
|
||||||
|
packageNames: string[],
|
||||||
|
platformId: string
|
||||||
|
): Promise<Package[]> {
|
||||||
|
if (packageNames.length === 0) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch packages by exact name match
|
||||||
|
const packages: Package[] = []
|
||||||
|
|
||||||
|
for (const name of packageNames) {
|
||||||
|
const result = await PackageService.getMany({
|
||||||
|
platform_id: platformId,
|
||||||
|
search: name,
|
||||||
|
limit: 1,
|
||||||
|
sort_by: 'popularity_score',
|
||||||
|
sort_order: 'desc'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Only add if exact match
|
||||||
|
if (result.packages.length > 0 && result.packages[0].name === name) {
|
||||||
|
packages.push(result.packages[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching preset packages:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch packages based on categories
|
||||||
|
*/
|
||||||
|
private static async fetchCategoryPackages(
|
||||||
|
categories: UserCategory[],
|
||||||
|
platformId: string,
|
||||||
|
limit: number
|
||||||
|
): Promise<Package[]> {
|
||||||
|
try {
|
||||||
|
// Map user categories to database categories
|
||||||
|
const categoryMap: Record<UserCategory, string[]> = {
|
||||||
|
'development': ['Development', 'Internet'],
|
||||||
|
'design': ['Graphics', 'Multimedia'],
|
||||||
|
'multimedia': ['Multimedia', 'Graphics'],
|
||||||
|
'system-tools': ['System', 'Utilities'],
|
||||||
|
'gaming': ['Games'],
|
||||||
|
'productivity': ['Office', 'Utilities'],
|
||||||
|
'education': ['Science', 'Education']
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all matching packages
|
||||||
|
const packages: Package[] = []
|
||||||
|
|
||||||
|
for (const category of categories) {
|
||||||
|
const dbCategories = categoryMap[category] || []
|
||||||
|
|
||||||
|
// Note: Since we don't have category filtering in current API,
|
||||||
|
// we'll fetch by popularity and filter client-side
|
||||||
|
// This is a limitation of current schema - categories are not well-utilized
|
||||||
|
const result = await PackageService.getMany({
|
||||||
|
platform_id: platformId,
|
||||||
|
limit: Math.ceil(limit / categories.length),
|
||||||
|
sort_by: 'popularity_score',
|
||||||
|
sort_order: 'desc'
|
||||||
|
})
|
||||||
|
|
||||||
|
packages.push(...result.packages)
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching category packages:', error)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove duplicate packages (by ID)
|
||||||
|
*/
|
||||||
|
private static deduplicatePackages(packages: Package[]): Package[] {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
return packages.filter(pkg => {
|
||||||
|
if (seen.has(pkg.id)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
seen.add(pkg.id)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Score a package based on multiple factors
|
||||||
|
*/
|
||||||
|
private static scorePackage(
|
||||||
|
pkg: Package,
|
||||||
|
categories: UserCategory[],
|
||||||
|
platformId: string,
|
||||||
|
presetPackageNames: string[],
|
||||||
|
experienceLevel?: ExperienceLevel
|
||||||
|
): RecommendedPackage {
|
||||||
|
let score = 0
|
||||||
|
let reason = ''
|
||||||
|
const isPresetMatch = presetPackageNames.includes(pkg.name)
|
||||||
|
|
||||||
|
// 1. Category Match Score (40%)
|
||||||
|
// For preset packages, this is always high
|
||||||
|
const categoryScore = isPresetMatch ? 1.0 : 0.5
|
||||||
|
score += categoryScore * SCORING_WEIGHTS.CATEGORY_MATCH
|
||||||
|
|
||||||
|
// 2. Popularity Score (30%)
|
||||||
|
// Normalize popularity_score (0-100) to 0-1
|
||||||
|
const popularityScore = (pkg.popularity_score || 0) / 100
|
||||||
|
score += popularityScore * SCORING_WEIGHTS.POPULARITY
|
||||||
|
|
||||||
|
// 3. OS Compatibility Score (20%)
|
||||||
|
// All packages from DB should be compatible, so this is always 1.0
|
||||||
|
const osScore = 1.0
|
||||||
|
score += osScore * SCORING_WEIGHTS.OS_COMPATIBILITY
|
||||||
|
|
||||||
|
// 4. Preset Boost (10%)
|
||||||
|
// Extra boost for preset packages based on priority
|
||||||
|
let presetBoost = 0
|
||||||
|
if (isPresetMatch) {
|
||||||
|
const priority = getPresetPriority(pkg.name, categories, platformId)
|
||||||
|
if (priority !== null) {
|
||||||
|
presetBoost = priority / 10 // Normalize 1-10 to 0.1-1.0
|
||||||
|
|
||||||
|
// Get recommendation reason from preset
|
||||||
|
const presetReason = getRecommendationReason(pkg.name, categories)
|
||||||
|
if (presetReason) {
|
||||||
|
reason = presetReason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
score += presetBoost * SCORING_WEIGHTS.PRESET_BOOST
|
||||||
|
|
||||||
|
// Default reason if not from preset
|
||||||
|
if (!reason) {
|
||||||
|
if (pkg.popularity_score && pkg.popularity_score > 70) {
|
||||||
|
reason = 'Popular choice in the community'
|
||||||
|
} else {
|
||||||
|
reason = 'Recommended for your selected categories'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize final score to 0-100
|
||||||
|
const finalScore = Math.round(score * 100)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: pkg.id,
|
||||||
|
name: pkg.name,
|
||||||
|
description: pkg.description || 'No description available',
|
||||||
|
version: pkg.version || 'latest',
|
||||||
|
category: typeof pkg.category === 'string' ? pkg.category : pkg.category?.name,
|
||||||
|
license: typeof pkg.license === 'string' ? pkg.license : pkg.license?.name,
|
||||||
|
type: pkg.type || 'cli',
|
||||||
|
platform: pkg.platform,
|
||||||
|
platform_id: pkg.platform_id,
|
||||||
|
repository: pkg.repository || 'official',
|
||||||
|
download_url: pkg.download_url,
|
||||||
|
lastUpdated: pkg.last_updated ? pkg.last_updated.toString() : undefined,
|
||||||
|
downloads: pkg.downloads_count,
|
||||||
|
popularity: pkg.popularity_score,
|
||||||
|
popularity_score: pkg.popularity_score,
|
||||||
|
tags: pkg.tags,
|
||||||
|
recommendationScore: finalScore,
|
||||||
|
recommendationReason: reason,
|
||||||
|
presetMatch: isPresetMatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get quick start recommendations (top 5 most essential)
|
||||||
|
*/
|
||||||
|
static async getQuickStartRecommendations(
|
||||||
|
platformId: string,
|
||||||
|
primaryCategory: UserCategory
|
||||||
|
): Promise<RecommendedPackage[]> {
|
||||||
|
return this.generateRecommendations({
|
||||||
|
platform_id: platformId,
|
||||||
|
categories: [primaryCategory],
|
||||||
|
limit: 5
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recommendations for multiple categories with balanced distribution
|
||||||
|
*/
|
||||||
|
static async getBalancedRecommendations(
|
||||||
|
platformId: string,
|
||||||
|
categories: UserCategory[],
|
||||||
|
totalLimit: number = 20
|
||||||
|
): Promise<RecommendedPackage[]> {
|
||||||
|
const perCategory = Math.ceil(totalLimit / categories.length)
|
||||||
|
const allRecommendations: RecommendedPackage[] = []
|
||||||
|
|
||||||
|
for (const category of categories) {
|
||||||
|
const recommendations = await this.generateRecommendations({
|
||||||
|
platform_id: platformId,
|
||||||
|
categories: [category],
|
||||||
|
limit: perCategory
|
||||||
|
})
|
||||||
|
allRecommendations.push(...recommendations)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deduplicate by ID and re-sort
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const deduplicated = allRecommendations.filter(pkg => {
|
||||||
|
if (seen.has(pkg.id)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
seen.add(pkg.id)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
deduplicated.sort((a, b) => b.recommendationScore - a.recommendationScore)
|
||||||
|
|
||||||
|
return deduplicated.slice(0, totalLimit)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { Package, Platform } from './index'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User category types for package recommendations
|
||||||
|
*/
|
||||||
|
export type UserCategory =
|
||||||
|
| 'development'
|
||||||
|
| 'design'
|
||||||
|
| 'multimedia'
|
||||||
|
| 'system-tools'
|
||||||
|
| 'gaming'
|
||||||
|
| 'productivity'
|
||||||
|
| 'education'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User experience level
|
||||||
|
*/
|
||||||
|
export type ExperienceLevel = 'beginner' | 'intermediate' | 'advanced'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User profile stored in localStorage
|
||||||
|
*/
|
||||||
|
export interface UserProfile {
|
||||||
|
categories: UserCategory[]
|
||||||
|
detectedOS?: string
|
||||||
|
selectedOS?: string // Manual override
|
||||||
|
experienceLevel?: ExperienceLevel
|
||||||
|
hasCompletedOnboarding: boolean
|
||||||
|
createdAt: string
|
||||||
|
lastUpdated: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request payload for recommendation API
|
||||||
|
*/
|
||||||
|
export interface RecommendationRequest {
|
||||||
|
platform_id: string
|
||||||
|
categories: UserCategory[]
|
||||||
|
experienceLevel?: ExperienceLevel
|
||||||
|
limit?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recommended package with score
|
||||||
|
*/
|
||||||
|
export interface RecommendedPackage {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
version: string
|
||||||
|
category?: string
|
||||||
|
license?: string
|
||||||
|
type: 'gui' | 'cli'
|
||||||
|
platform?: string | any
|
||||||
|
platform_id?: string
|
||||||
|
repository: 'official' | 'third-party' | 'aur'
|
||||||
|
download_url?: string
|
||||||
|
lastUpdated?: string
|
||||||
|
downloads?: number
|
||||||
|
popularity?: number
|
||||||
|
popularity_score?: number
|
||||||
|
tags?: string[]
|
||||||
|
recommendationScore: number
|
||||||
|
recommendationReason: string
|
||||||
|
presetMatch?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preset package configuration
|
||||||
|
*/
|
||||||
|
export interface PackagePreset {
|
||||||
|
packageName: string
|
||||||
|
platforms: string[] // ['windows', 'macos', 'ubuntu', 'arch', 'fedora']
|
||||||
|
priority: number // 1-10, higher = more important
|
||||||
|
reason: string // Why this package is recommended
|
||||||
|
experienceLevel?: ExperienceLevel[] // Target experience levels
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Category preset configuration
|
||||||
|
*/
|
||||||
|
export interface CategoryPreset {
|
||||||
|
category: UserCategory
|
||||||
|
packages: PackagePreset[]
|
||||||
|
description: string
|
||||||
|
icon: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recommendation response
|
||||||
|
*/
|
||||||
|
export interface RecommendationResponse {
|
||||||
|
recommendations: RecommendedPackage[]
|
||||||
|
total: number
|
||||||
|
userProfile: {
|
||||||
|
categories: UserCategory[]
|
||||||
|
platform: string
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
-4
@@ -1,7 +1,12 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": ["dom", "dom.iterable", "es6"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"es6",
|
||||||
|
"es2017"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -20,9 +25,18 @@
|
|||||||
],
|
],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
"exclude": ["node_modules"]
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user