Omarrran's picture
Add EduRecommender HuggingFace Spaces app
5bd3663
/**
* 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>&#9888;</span> {error}
</div>
)}
{data?.recommendations?.length > 0 && !loading && (
<RecommendationGrid recommendations={data.recommendations} />
)}
<ContentBrowser items={content} />
</main>
</div>
);
}