/** * Root application component. * * Layout: sidebar (profile form) + main area (hero + results). * Auto-mode (on by default): selecting a preset auto-fires recommendations. * Custom profile: blank slate for manual entry. */ import React, { useState, useEffect, useCallback, useRef } from "react"; import { useRecommend } from "./hooks/useRecommend.js"; import Header from "./components/Header.jsx"; import Sidebar from "./components/Sidebar.jsx"; import RecommendationGrid from "./components/RecommendationGrid.jsx"; import PipelineToast from "./components/PipelineToast.jsx"; import ContentBrowser from "./components/ContentBrowser.jsx"; import styles from "./App.module.css"; /* ── Preset profiles ─────────────────────────────────────────────────── */ const PRESETS = [ { label: "Alice \u2014 ML Deployment", value: { user_id: "u1", name: "Alice", goal: "Learn to deploy ML models into production using Kubernetes and cloud platforms", learning_style: "visual", preferred_difficulty: "Intermediate", time_per_day: 60, viewed_content_ids: [1], interest_tags: ["ml", "deployment", "kubernetes", "docker"], }, }, { label: "Bob \u2014 Data Science Beginner", value: { user_id: "u2", name: "Bob", goal: "Transition from software engineering to data science and machine learning", learning_style: "hands-on", preferred_difficulty: "Beginner", time_per_day: 45, viewed_content_ids: [7], interest_tags: ["python", "data-science", "ml", "numpy"], }, }, { label: "Carol \u2014 Advanced NLP", value: { user_id: "u3", name: "Carol", goal: "Master advanced NLP and LLM techniques for building AI-powered applications", learning_style: "reading", preferred_difficulty: "Advanced", time_per_day: 90, viewed_content_ids: [5], interest_tags: ["nlp", "transformers", "llm", "prompt-engineering"], }, }, ]; const EMPTY_PROFILE = { user_id: "custom", name: "", goal: "", learning_style: "visual", preferred_difficulty: "Intermediate", time_per_day: 60, viewed_content_ids: [], interest_tags: [], }; export default function App() { const { data, loading, error, fetchRecommendations } = useRecommend(); const [profile, setProfile] = useState(PRESETS[0].value); const [sidebarOpen, setSidebarOpen] = useState(true); const [content, setContent] = useState([]); const [autoMode, setAutoMode] = useState(true); const [activePreset, setActivePreset] = useState(0); // -1 = custom const lastFiredPreset = useRef(-999); /* Fetch content catalogue once */ useEffect(() => { fetch("/api/content") .then((r) => r.json()) .then(setContent) .catch(() => {}); }, []); /* Auto-mode: fire when a preset is selected (and it changed) */ useEffect(() => { if (!autoMode) return; if (activePreset < 0) return; if (activePreset === lastFiredPreset.current) return; lastFiredPreset.current = activePreset; const p = PRESETS[activePreset]?.value; if (p?.goal?.trim() && p?.interest_tags?.length) { fetchRecommendations(p); } }, [activePreset, autoMode, fetchRecommendations]); const handleSubmit = useCallback(() => { if (!profile.goal?.trim() || !profile.interest_tags?.length) return; fetchRecommendations(profile); }, [profile, fetchRecommendations]); const handlePreset = useCallback((idx) => { if (idx < 0) { setProfile({ ...EMPTY_PROFILE }); setActivePreset(-1); } else { setProfile(PRESETS[idx].value); setActivePreset(idx); } }, []); const handleProfileChange = useCallback((updated) => { setProfile(updated); // If user edits any field, mark as custom const match = PRESETS.findIndex( (p) => p.value.user_id === updated.user_id ); if (match < 0) { setActivePreset(-1); } }, []); return (