Spaces:
Sleeping
Sleeping
| /** | |
| * 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 ( | |
| <div className={styles.layout}> | |
| <Sidebar | |
| open={sidebarOpen} | |
| onToggle={() => setSidebarOpen((p) => !p)} | |
| profile={profile} | |
| onChange={handleProfileChange} | |
| presets={PRESETS} | |
| activePreset={activePreset} | |
| onPreset={handlePreset} | |
| onSubmit={handleSubmit} | |
| loading={loading} | |
| autoMode={autoMode} | |
| onAutoModeChange={setAutoMode} | |
| /> | |
| <main className={`${styles.main} ${sidebarOpen ? "" : styles.mainFull}`}> | |
| <Header /> | |
| {loading && <PipelineToast steps={[]} loading />} | |
| {data && !loading && ( | |
| <PipelineToast | |
| steps={data.pipeline_log} | |
| totalMs={data.total_duration_ms} | |
| /> | |
| )} | |
| {error && ( | |
| <div className={styles.error}> | |
| <span>⚠</span> {error} | |
| </div> | |
| )} | |
| {data?.recommendations?.length > 0 && !loading && ( | |
| <RecommendationGrid recommendations={data.recommendations} /> | |
| )} | |
| <ContentBrowser items={content} /> | |
| </main> | |
| </div> | |
| ); | |
| } | |