diff --git a/README.md b/README.md
index 156e2dc..8ce47b8 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@
| Feature | Description |
|---------|-------------|
+| ๐ง **AI Learning Assistant** | Personalized curriculum recommendations, semantic search, milestone roadmaps & interactive tutoring |
| ๐ค **AI Tutor** | Google Gemini-powered chatbot for instant answers |
| ๐ **Progress Tracking** | LocalStorage-backed topic completion with visual progress bar |
| ๐ฎ **Interactive Simulations** | Motion, Projectile, NLM, Ray Optics, Chemical Bonding, Thermodynamics |
@@ -99,15 +100,54 @@ LearnSphere_2/
---
+## ๐ง AI Learning Assistant & Structured Content Engine
+
+LearnSphere includes a personalized AI Learning Assistant that analyzes a learner's background, timeline, and goals (e.g. *"I know basic JavaScript and want to become a React developer in 3 months"*), dynamically searches our structured curriculum database, and generates customized milestone roadmaps.
+
+### ๐ The End-to-End Flow
+```
+Create Structured Content โ Store in SQLite DB โ AI Semantic Retrieval โ User Query Understanding โ Personalized Roadmap & Next Step
+```
+
+### ๐ Structured Content Catalog (8 Technical Tracks)
+1. **JavaScript**: Modern JavaScript: Core to Advanced ES6+ (Beginner to Intermediate)
+2. **React**: Modern Frontend Engineering & State Architecture (Intermediate)
+3. **Node.js**: Scalable Backend Architecture & Express APIs (Intermediate)
+4. **Python**: Python Programming & Algorithmic Problem Solving (Beginner)
+5. **Java**: Object-Oriented Engineering & Spring Boot Microservices (Intermediate)
+6. **SQL**: Relational Database Architecture, CTEs & Index Optimization (Beginner)
+7. **Machine Learning**: Foundations & Applied Scikit-Learn Pipelines (Intermediate)
+8. **Full Stack Web Development**: MERN & Modern Web Architecture Capstone (Advanced)
+
+Each course contains detailed prerequisites, topics covered, estimated hours, curriculum sequence modules, resource types (labs, videos, projects, quizzes), and hands-on capstone projects.
+
+### ๐ ๏ธ Architecture & Capabilities
+- **Backend API**: FastAPI with Pydantic validation, CORS, error handling, and OpenAPI documentation (`/docs`).
+- **Storage**: SQLite database (`backend/data/learnsphere_assistant.db`) storing courses, queries, and conversational chat history.
+- **AI & Semantic Search**:
+ - TF-IDF vectorizer + Cosine Similarity scoring over course documents.
+ - Automatic prerequisite dependency chaining and progression ordering.
+ - Google Gemini integration (`gemini-2.5-flash` / `gemini-1.5-flash`) via `google-genai`.
+ - Deterministic semantic fallback engine ensuring 100% functionality offline and without an API key.
+- **Frontend Interface**:
+ - `assistant.html`: Fully responsive UI with dark/light mode support.
+ - Visual milestone timeline, why-recommended explanation, and immediate next step callout.
+ - Interactive follow-up conversational assistant.
+ - Structured content catalog explorer with live search and filtering.
+ - Past guidance history drawer.
+
+---
+
## ๐ฅ๏ธ Tech Stack
| Layer | Technology |
|-------|-----------|
-| Frontend | HTML5, CSS3, Vanilla JavaScript |
-| Backend (AI Tutor) | Python 3, Flask, flask-cors |
-| AI Engine | Google Gemini 1.5 Pro (via `google-generativeai`) |
-| Data Persistence | Browser localStorage (progress, theme, auth) |
-| CI/CD | GitHub Actions (HTML validation, secret scanning) |
+| Frontend | HTML5, CSS3, Vanilla JavaScript, Vite |
+| Backend (AI Assistant) | Python 3, FastAPI, Uvicorn, Pydantic, Scikit-learn |
+| Backend (Auth Server) | Node.js, Express, SQLite |
+| AI Engine | Google Gemini (`google-genai`) + TF-IDF Semantic Retrieval Fallback |
+| Database | SQLite (`learnsphere_assistant.db` & `learnsphere.db`) |
+| Testing | Vitest (51 tests) & Pytest (11 tests) |
---
diff --git a/assistant.css b/assistant.css
new file mode 100644
index 0000000..7286511
--- /dev/null
+++ b/assistant.css
@@ -0,0 +1,1432 @@
+/* assistant.css โ LearnSphere AI Learning Assistant */
+
+.assistant-container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 30px 20px 80px 20px;
+}
+
+/* Hero Section */
+.assistant-hero {
+ text-align: center;
+ margin-bottom: 36px;
+}
+
+.ai-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ background: rgba(56, 189, 248, 0.12);
+ border: 1px solid rgba(56, 189, 248, 0.3);
+ color: var(--accent-color);
+ padding: 6px 16px;
+ border-radius: 9999px;
+ font-size: 0.85rem;
+ font-weight: 600;
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+ margin-bottom: 16px;
+}
+
+.assistant-hero h1 {
+ font-size: 2.5rem;
+ font-weight: 800;
+ margin-bottom: 12px;
+ background: linear-gradient(135deg, #ffffff 30%, var(--accent-color) 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+[data-theme="light"] .assistant-hero h1 {
+ background: linear-gradient(135deg, #0f172a 30%, #0284c7 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+.assistant-hero p {
+ color: var(--text-muted);
+ font-size: 1.1rem;
+ max-width: 680px;
+ margin: 0 auto;
+ line-height: 1.6;
+}
+
+/* Tab Navigation */
+.view-tabs {
+ display: flex;
+ justify-content: center;
+ gap: 12px;
+ margin-bottom: 30px;
+}
+
+.tab-btn {
+ background: var(--card-bg);
+ border: 1px solid var(--card-border);
+ color: var(--text-muted);
+ padding: 10px 22px;
+ border-radius: 30px;
+ font-weight: 600;
+ font-size: 0.95rem;
+ cursor: pointer;
+ transition: all 0.25s ease;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.tab-btn:hover {
+ color: var(--text-color);
+ border-color: var(--accent-color);
+}
+
+.tab-btn.active {
+ background: var(--accent-color);
+ color: #0b0f19;
+ border-color: var(--accent-color);
+ font-weight: 700;
+ box-shadow: 0 4px 14px rgba(56, 189, 248, 0.35);
+}
+
+/* Prompt Card */
+.prompt-card {
+ background: var(--card-bg);
+ border: 1px solid var(--card-border);
+ border-radius: 20px;
+ padding: 28px;
+ box-shadow: var(--card-shadow);
+ backdrop-filter: blur(12px);
+ margin-bottom: 36px;
+ position: relative;
+ overflow: hidden;
+}
+
+.prompt-card::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+ background: linear-gradient(90deg, #0284c7, #38bdf8, #818cf8);
+}
+
+.prompt-label {
+ display: block;
+ font-weight: 700;
+ font-size: 1rem;
+ color: var(--text-color);
+ margin-bottom: 10px;
+}
+
+.prompt-textarea {
+ width: 100%;
+ min-height: 110px;
+ background: rgba(15, 23, 42, 0.6);
+ border: 1px solid var(--border-color);
+ border-radius: 14px;
+ padding: 16px;
+ color: var(--text-color);
+ font-family: inherit;
+ font-size: 1rem;
+ line-height: 1.5;
+ resize: vertical;
+ transition: border-color 0.2s, box-shadow 0.2s;
+ box-sizing: border-box;
+}
+
+[data-theme="light"] .prompt-textarea {
+ background: rgba(255, 255, 255, 0.9);
+}
+
+.prompt-textarea:focus {
+ outline: none;
+ border-color: var(--accent-color);
+ box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.2);
+}
+
+/* Quick Suggestion Chips */
+.quick-prompts-label {
+ font-size: 0.85rem;
+ color: var(--text-muted);
+ margin-top: 14px;
+ margin-bottom: 8px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.chips-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 20px;
+}
+
+.chip-btn {
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ color: var(--text-muted);
+ font-size: 0.85rem;
+ padding: 6px 12px;
+ border-radius: 20px;
+ cursor: pointer;
+ transition: all 0.2s;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+[data-theme="light"] .chip-btn {
+ background: rgba(0, 0, 0, 0.04);
+ border-color: rgba(0, 0, 0, 0.1);
+}
+
+.chip-btn:hover {
+ background: rgba(56, 189, 248, 0.15);
+ border-color: var(--accent-color);
+ color: var(--accent-color);
+ transform: translateY(-1px);
+}
+
+/* Refinement Options Row */
+.options-row {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+ margin-bottom: 22px;
+ padding-top: 16px;
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
+}
+
+.option-group label {
+ display: block;
+ font-size: 0.82rem;
+ font-weight: 600;
+ color: var(--text-muted);
+ margin-bottom: 6px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.option-select {
+ width: 100%;
+ padding: 9px 12px;
+ border-radius: 10px;
+ background: rgba(15, 23, 42, 0.6);
+ border: 1px solid var(--border-color);
+ color: var(--text-color);
+ font-size: 0.9rem;
+ cursor: pointer;
+}
+
+[data-theme="light"] .option-select {
+ background: rgba(255, 255, 255, 0.9);
+}
+
+.option-select:focus {
+ outline: none;
+ border-color: var(--accent-color);
+}
+
+.submit-row {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 16px;
+}
+
+.generate-btn {
+ background: linear-gradient(135deg, #0284c7, #38bdf8);
+ color: #ffffff;
+ border: none;
+ padding: 12px 28px;
+ border-radius: 12px;
+ font-size: 1rem;
+ font-weight: 700;
+ cursor: pointer;
+ transition: all 0.25s ease;
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ box-shadow: 0 4px 15px rgba(2, 132, 199, 0.4);
+}
+
+.generate-btn:hover:not(:disabled) {
+ background: linear-gradient(135deg, #0369a1, #0284c7);
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(2, 132, 199, 0.5);
+}
+
+.generate-btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none;
+}
+
+/* Loading State */
+.loading-box {
+ display: none;
+ text-align: center;
+ padding: 60px 24px;
+ background: var(--card-bg);
+ border: 1px solid var(--card-border);
+ border-radius: 20px;
+ box-shadow: var(--card-shadow);
+}
+
+/* Modern Dual-Ring AI Spinner */
+.loading-spinner-wrapper {
+ position: relative;
+ width: 68px;
+ height: 68px;
+ margin: 0 auto 24px auto;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.spinner-outer-ring {
+ position: absolute;
+ inset: 0;
+ border-radius: 50%;
+ border: 3.5px solid rgba(56, 189, 248, 0.15);
+ border-top: 3.5px solid var(--accent-color);
+ border-right: 3.5px solid #818cf8;
+ -webkit-animation: spinClockwise 1s linear infinite !important;
+ animation: spinClockwise 1s linear infinite !important;
+ box-shadow: 0 0 16px rgba(56, 189, 248, 0.3);
+}
+
+.spinner-inner-ring {
+ position: absolute;
+ inset: 8px;
+ border-radius: 50%;
+ border: 3px solid rgba(129, 140, 248, 0.1);
+ border-top: 3px solid #38bdf8;
+ border-bottom: 3px solid #818cf8;
+ -webkit-animation: spinCounter 0.75s linear infinite !important;
+ animation: spinCounter 0.75s linear infinite !important;
+}
+
+.spinner-center-icon {
+ font-size: 1.35rem;
+ color: var(--accent-color);
+ -webkit-animation: pulseGlowIcon 1.5s ease-in-out infinite !important;
+ animation: pulseGlowIcon 1.5s ease-in-out infinite !important;
+}
+
+/* Fallback .spinner */
+.spinner {
+ width: 52px;
+ height: 52px;
+ border: 4px solid rgba(56, 189, 248, 0.2);
+ border-top: 4px solid var(--accent-color);
+ border-right: 4px solid #818cf8;
+ border-radius: 50%;
+ -webkit-animation: spinClockwise 0.9s linear infinite !important;
+ animation: spinClockwise 0.9s linear infinite !important;
+ margin: 0 auto 20px auto;
+ box-shadow: 0 0 16px rgba(56, 189, 248, 0.25);
+}
+
+@-webkit-keyframes spinClockwise {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes spinClockwise {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+}
+
+@-webkit-keyframes spinCounter {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(-360deg);
+ transform: rotate(-360deg);
+ }
+}
+
+@keyframes spinCounter {
+ 0% {
+ -webkit-transform: rotate(0deg);
+ transform: rotate(0deg);
+ }
+ 100% {
+ -webkit-transform: rotate(-360deg);
+ transform: rotate(-360deg);
+ }
+}
+
+@-webkit-keyframes pulseGlowIcon {
+ 0%, 100% {
+ opacity: 0.65;
+ -webkit-transform: scale(0.92);
+ transform: scale(0.92);
+ }
+ 50% {
+ opacity: 1;
+ -webkit-transform: scale(1.12);
+ transform: scale(1.12);
+ }
+}
+
+@keyframes pulseGlowIcon {
+ 0%, 100% {
+ opacity: 0.65;
+ transform: scale(0.92);
+ }
+ 50% {
+ opacity: 1;
+ transform: scale(1.12);
+ }
+}
+
+/* Ensure spinners always rotate even if user has reduced-motion active */
+@media (prefers-reduced-motion: reduce) {
+ .spinner-outer-ring,
+ .spinner-inner-ring,
+ .spinner-center-icon,
+ .spinner,
+ .fa-spin {
+ animation-duration: 1.2s !important;
+ animation-iteration-count: infinite !important;
+ -webkit-animation-iteration-count: infinite !important;
+ }
+}
+
+html[data-reduced-motion="true"] .spinner-outer-ring,
+html[data-reduced-motion="true"] .spinner-inner-ring,
+html[data-reduced-motion="true"] .spinner-center-icon,
+html[data-reduced-motion="true"] .spinner,
+html[data-reduced-motion="true"] .fa-spin {
+ animation-duration: 1.2s !important;
+ animation-iteration-count: infinite !important;
+ -webkit-animation-iteration-count: infinite !important;
+}
+
+.loading-box h3 {
+ font-size: 1.3rem;
+ margin-bottom: 8px;
+ color: var(--text-color);
+}
+
+.loading-ticker {
+ color: var(--text-muted);
+ font-size: 0.95rem;
+ min-height: 24px;
+}
+
+/* Error State */
+.error-box {
+ display: none;
+ background: rgba(239, 68, 68, 0.1);
+ border: 1px solid rgba(239, 68, 68, 0.3);
+ color: #f87171;
+ padding: 20px;
+ border-radius: 14px;
+ margin-bottom: 24px;
+ text-align: center;
+}
+
+.error-box button {
+ background: #ef4444;
+ color: #fff;
+ border: none;
+ padding: 6px 16px;
+ border-radius: 8px;
+ cursor: pointer;
+ margin-top: 10px;
+ font-weight: 600;
+}
+
+/* Recommendation Results Section */
+.results-wrapper {
+ display: none;
+ animation: fadeIn 0.4s ease;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* Learner Profile Banner */
+.profile-pill-strip {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin-bottom: 24px;
+}
+
+.profile-pill {
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid var(--border-color);
+ padding: 8px 16px;
+ border-radius: 30px;
+ font-size: 0.88rem;
+ color: var(--text-color);
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.profile-pill strong {
+ color: var(--accent-color);
+}
+
+/* Summary Card */
+.overview-card {
+ background: var(--card-bg);
+ border: 1px solid var(--card-border);
+ border-radius: 18px;
+ padding: 26px;
+ margin-bottom: 24px;
+ box-shadow: var(--card-shadow);
+}
+
+.overview-card h2 {
+ font-size: 1.4rem;
+ color: var(--text-color);
+ margin-bottom: 12px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.overview-card p {
+ color: var(--text-muted);
+ line-height: 1.65;
+ font-size: 1rem;
+ margin-bottom: 14px;
+}
+
+.rationale-callout {
+ background: rgba(56, 189, 248, 0.07);
+ border-left: 4px solid var(--accent-color);
+ padding: 14px 18px;
+ border-radius: 0 10px 10px 0;
+ color: var(--text-color);
+ font-size: 0.95rem;
+ line-height: 1.55;
+ margin-top: 14px;
+}
+
+/* Immediate Next Step Card */
+.next-step-card {
+ background: linear-gradient(135deg, rgba(2, 132, 199, 0.15), rgba(56, 189, 248, 0.05));
+ border: 1px solid rgba(56, 189, 248, 0.35);
+ border-radius: 18px;
+ padding: 24px;
+ margin-bottom: 30px;
+ display: flex;
+ align-items: flex-start;
+ gap: 20px;
+}
+
+.next-step-icon {
+ font-size: 2rem;
+ color: var(--accent-color);
+ background: rgba(56, 189, 248, 0.15);
+ width: 56px;
+ height: 56px;
+ border-radius: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.next-step-content h3 {
+ font-size: 1.15rem;
+ color: var(--text-color);
+ margin-bottom: 6px;
+ font-weight: 700;
+}
+
+.next-step-content p {
+ color: var(--text-muted);
+ font-size: 0.98rem;
+ line-height: 1.55;
+ margin: 0;
+}
+
+/* Learning Sequence Roadmap */
+.section-heading {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--text-color);
+ margin-bottom: 20px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.roadmap-timeline {
+ position: relative;
+ padding-left: 36px;
+ margin-bottom: 40px;
+}
+
+.roadmap-timeline::before {
+ content: "";
+ position: absolute;
+ left: 14px;
+ top: 10px;
+ bottom: 20px;
+ width: 3px;
+ background: linear-gradient(180deg, var(--accent-color), #818cf8);
+ border-radius: 2px;
+}
+
+.timeline-phase {
+ position: relative;
+ margin-bottom: 28px;
+}
+
+.phase-node {
+ position: absolute;
+ left: -36px;
+ top: 4px;
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ background: #0284c7;
+ border: 3px solid #0b0f19;
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 700;
+ font-size: 0.8rem;
+ box-shadow: 0 0 10px rgba(56, 189, 248, 0.5);
+}
+
+.phase-card {
+ background: var(--card-bg);
+ border: 1px solid var(--card-border);
+ border-radius: 14px;
+ padding: 20px;
+ box-shadow: var(--card-shadow);
+ transition: transform 0.2s, border-color 0.2s;
+}
+
+.phase-card:hover {
+ border-color: var(--accent-color);
+ transform: translateX(4px);
+}
+
+.phase-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 10px;
+ margin-bottom: 10px;
+}
+
+.phase-title {
+ font-size: 1.15rem;
+ font-weight: 700;
+ color: var(--text-color);
+}
+
+.phase-duration {
+ background: rgba(255, 255, 255, 0.08);
+ padding: 4px 10px;
+ border-radius: 20px;
+ font-size: 0.82rem;
+ font-weight: 600;
+ color: var(--accent-color);
+}
+
+.phase-details {
+ color: var(--text-muted);
+ font-size: 0.92rem;
+ line-height: 1.5;
+ margin-bottom: 12px;
+}
+
+.phase-project {
+ background: rgba(16, 185, 129, 0.1);
+ border: 1px solid rgba(16, 185, 129, 0.25);
+ color: #10b981;
+ padding: 8px 12px;
+ border-radius: 8px;
+ font-size: 0.86rem;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-weight: 600;
+}
+
+/* Recommended Courses Grid */
+.courses-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
+ gap: 24px;
+ margin-bottom: 40px;
+}
+
+.course-card {
+ background: var(--card-bg);
+ border: 1px solid var(--card-border);
+ border-radius: 18px;
+ padding: 24px;
+ box-shadow: var(--card-shadow);
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ transition: transform 0.25s, border-color 0.25s;
+ position: relative;
+ overflow: hidden;
+}
+
+.course-card:hover {
+ transform: translateY(-4px);
+ border-color: var(--accent-color);
+}
+
+.course-top-badges {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+}
+
+.badge-topic {
+ font-size: 0.8rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ color: var(--accent-color);
+}
+
+.badge-diff {
+ font-size: 0.75rem;
+ font-weight: 700;
+ padding: 3px 8px;
+ border-radius: 6px;
+ text-transform: uppercase;
+}
+
+.diff-beginner { background: rgba(16, 185, 129, 0.15); color: #10b981; }
+.diff-intermediate { background: rgba(245, 158, 11, 0.15); color: #f59e0b; }
+.diff-advanced { background: rgba(239, 68, 68, 0.15); color: #ef4444; }
+
+.course-card h3 {
+ font-size: 1.25rem;
+ color: var(--text-color);
+ margin-bottom: 10px;
+ font-weight: 700;
+}
+
+.course-card p {
+ color: var(--text-muted);
+ font-size: 0.92rem;
+ line-height: 1.5;
+ margin-bottom: 16px;
+}
+
+.course-meta {
+ display: flex;
+ gap: 14px;
+ font-size: 0.84rem;
+ color: var(--text-muted);
+ margin-bottom: 14px;
+}
+
+.course-resource-pills {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-bottom: 18px;
+}
+
+.res-pill {
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ font-size: 0.75rem;
+ padding: 3px 8px;
+ border-radius: 4px;
+ color: var(--text-muted);
+}
+
+.modules-toggle {
+ background: transparent;
+ border: 1px solid var(--border-color);
+ color: var(--text-color);
+ padding: 8px 14px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 0.85rem;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ margin-bottom: 12px;
+}
+
+.modules-accordion {
+ display: none;
+ background: rgba(15, 23, 42, 0.4);
+ border-radius: 8px;
+ padding: 12px;
+ margin-bottom: 14px;
+ font-size: 0.85rem;
+}
+
+[data-theme="light"] .modules-accordion {
+ background: rgba(0, 0, 0, 0.03);
+}
+
+.module-item {
+ padding: 6px 0;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+.module-item:last-child {
+ border-bottom: none;
+}
+
+.module-item-title {
+ font-weight: 600;
+ color: var(--text-color);
+}
+
+.module-item-project {
+ color: var(--text-muted);
+ font-size: 0.8rem;
+ margin-top: 2px;
+}
+
+.capstone-box {
+ background: rgba(56, 189, 248, 0.06);
+ border: 1px dashed rgba(56, 189, 248, 0.3);
+ border-radius: 8px;
+ padding: 10px 12px;
+ font-size: 0.84rem;
+ color: var(--text-color);
+ margin-bottom: 16px;
+}
+
+.capstone-box strong {
+ color: var(--accent-color);
+}
+
+/* Follow-up Interactive Chat */
+.assistant-chat-section {
+ background: var(--card-bg);
+ border: 1px solid var(--card-border);
+ border-radius: 20px;
+ padding: 26px;
+ box-shadow: var(--card-shadow);
+ margin-top: 40px;
+}
+
+.chat-messages-container {
+ max-height: 380px;
+ overflow-y: auto;
+ padding: 14px;
+ background: rgba(15, 23, 42, 0.4);
+ border-radius: 14px;
+ margin-bottom: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+[data-theme="light"] .chat-messages-container {
+ background: rgba(0, 0, 0, 0.03);
+}
+
+.chat-bubble {
+ max-width: 80%;
+ padding: 12px 16px;
+ border-radius: 14px;
+ line-height: 1.5;
+ font-size: 0.95rem;
+}
+
+.chat-bubble.user {
+ align-self: flex-end;
+ background: #0284c7;
+ color: #fff;
+ border-bottom-right-radius: 4px;
+}
+
+.chat-bubble.assistant {
+ align-self: flex-start;
+ background: rgba(255, 255, 255, 0.08);
+ color: var(--text-color);
+ border-bottom-left-radius: 4px;
+ border: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+[data-theme="light"] .chat-bubble.assistant {
+ background: #ffffff;
+ border-color: rgba(0, 0, 0, 0.08);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+}
+
+.chat-input-wrapper {
+ display: flex;
+ gap: 10px;
+}
+
+.chat-input {
+ flex: 1;
+ background: rgba(15, 23, 42, 0.6);
+ border: 1px solid var(--border-color);
+ color: var(--text-color);
+ padding: 12px 16px;
+ border-radius: 12px;
+ font-size: 0.95rem;
+}
+
+[data-theme="light"] .chat-input {
+ background: #ffffff;
+}
+
+.chat-input:focus {
+ outline: none;
+ border-color: var(--accent-color);
+}
+
+.chat-send-btn {
+ background: var(--accent-color);
+ color: #0b0f19;
+ border: none;
+ padding: 0 22px;
+ border-radius: 12px;
+ font-weight: 700;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.chat-send-btn:hover {
+ background: #0284c7;
+ color: #ffffff;
+}
+
+/* Catalog View Section */
+.catalog-section {
+ display: none;
+}
+
+.catalog-filters {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin-bottom: 24px;
+ align-items: center;
+}
+
+.catalog-search-input {
+ flex: 1;
+ min-width: 240px;
+ background: var(--card-bg);
+ border: 1px solid var(--card-border);
+ color: var(--text-color);
+ padding: 10px 16px;
+ border-radius: 10px;
+ font-size: 0.95rem;
+}
+
+.catalog-search-input:focus {
+ outline: none;
+ border-color: var(--accent-color);
+}
+
+/* History Modal or Drawer */
+.history-item {
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 12px;
+ padding: 14px 18px;
+ margin-bottom: 12px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.history-item:hover {
+ border-color: var(--accent-color);
+ background: rgba(56, 189, 248, 0.05);
+}
+
+.history-query {
+ font-weight: 600;
+ color: var(--text-color);
+ margin-bottom: 4px;
+}
+
+.history-meta {
+ font-size: 0.8rem;
+ color: var(--text-muted);
+}
+
+/* Empty State */
+.empty-state {
+ text-align: center;
+ padding: 60px 20px;
+ color: var(--text-muted);
+}
+
+.empty-state i {
+ font-size: 3rem;
+ color: var(--accent-color);
+ margin-bottom: 16px;
+ opacity: 0.7;
+}
+
+.empty-state h3 {
+ color: var(--text-color);
+ font-size: 1.3rem;
+ margin-bottom: 8px;
+}
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+ .assistant-hero h1 {
+ font-size: 1.9rem;
+ }
+ .options-row {
+ grid-template-columns: 1fr;
+ }
+ .courses-grid {
+ grid-template-columns: 1fr;
+ }
+ .next-step-card {
+ flex-direction: column;
+ }
+ .roadmap-timeline {
+ padding-left: 28px;
+ }
+ .phase-node {
+ left: -28px;
+ }
+}
+
+/* โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+ DAG Graph & Visual Roadmap Enhancements
+ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ */
+
+/* Structured Takeaways Cards Grid (Replaces monolithic paragraphs) */
+.takeaways-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+ gap: 16px;
+ margin-top: 18px;
+}
+
+.takeaway-card {
+ background: rgba(15, 23, 42, 0.45);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 14px;
+ padding: 18px;
+ transition: transform 0.2s, border-color 0.2s;
+ display: flex;
+ flex-direction: column;
+}
+
+[data-theme="light"] .takeaway-card {
+ background: rgba(255, 255, 255, 0.9);
+ border-color: rgba(0, 0, 0, 0.08);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04);
+}
+
+.takeaway-card:hover {
+ transform: translateY(-2px);
+ border-color: var(--accent-color);
+}
+
+.takeaway-header {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 8px;
+}
+
+.takeaway-icon-box {
+ width: 36px;
+ height: 36px;
+ border-radius: 10px;
+ background: rgba(56, 189, 248, 0.15);
+ color: var(--accent-color);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1rem;
+ flex-shrink: 0;
+}
+
+.takeaway-title {
+ font-size: 0.95rem;
+ font-weight: 700;
+ color: var(--text-color);
+}
+
+.takeaway-desc {
+ color: var(--text-muted);
+ font-size: 0.88rem;
+ line-height: 1.5;
+ margin: 0;
+}
+
+/* Interactive Next Step Card */
+.next-step-detailed {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+ margin-top: 10px;
+}
+
+.next-step-badge-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 10px;
+}
+
+.next-step-badge {
+ background: rgba(56, 189, 248, 0.2);
+ border: 1px solid rgba(56, 189, 248, 0.4);
+ color: var(--accent-color);
+ padding: 4px 12px;
+ border-radius: 20px;
+ font-size: 0.8rem;
+ font-weight: 700;
+ text-transform: uppercase;
+}
+
+.next-step-checklist {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.next-step-item {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ font-size: 0.92rem;
+ color: var(--text-color);
+ line-height: 1.45;
+}
+
+.next-step-item i {
+ color: #10b981;
+ margin-top: 3px;
+}
+
+/* DAG Roadmap Flow Container */
+.dag-view-toggle {
+ display: inline-flex;
+ background: rgba(15, 23, 42, 0.6);
+ border: 1px solid var(--border-color);
+ border-radius: 30px;
+ padding: 4px;
+ margin-bottom: 24px;
+}
+
+[data-theme="light"] .dag-view-toggle {
+ background: rgba(0, 0, 0, 0.05);
+}
+
+.dag-view-btn {
+ background: transparent;
+ border: none;
+ color: var(--text-muted);
+ padding: 8px 18px;
+ border-radius: 20px;
+ font-size: 0.85rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.dag-view-btn.active {
+ background: var(--accent-color);
+ color: #0b0f19;
+ font-weight: 700;
+ box-shadow: 0 2px 8px rgba(56, 189, 248, 0.3);
+}
+
+/* DAG Visual Tree Layout */
+.dag-flow-tree {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ position: relative;
+ max-width: 900px;
+ margin: 0 auto;
+}
+
+.dag-node-wrapper {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ position: relative;
+}
+
+.dag-node-card {
+ width: 100%;
+ background: var(--card-bg);
+ border: 1px solid var(--card-border);
+ border-radius: 16px;
+ padding: 22px;
+ box-shadow: var(--card-shadow);
+ position: relative;
+ transition: all 0.25s ease;
+ backdrop-filter: blur(10px);
+}
+
+.dag-node-card:hover {
+ border-color: var(--accent-color);
+ transform: translateY(-2px);
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
+}
+
+.dag-node-card.active-node {
+ border-color: var(--accent-color);
+ box-shadow: 0 0 20px rgba(56, 189, 248, 0.25);
+ background: linear-gradient(135deg, rgba(30, 41, 59, 0.9), rgba(15, 23, 42, 0.9));
+}
+
+.dag-node-card.completed-node {
+ border-color: rgba(16, 185, 129, 0.5);
+ background: rgba(16, 185, 129, 0.04);
+}
+
+.dag-node-card.completed-node .dag-node-step {
+ background: #10b981;
+}
+
+/* Node Top Bar */
+.dag-node-top {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 12px;
+}
+
+.dag-node-left {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.dag-node-step {
+ background: #0284c7;
+ color: #fff;
+ font-size: 0.75rem;
+ font-weight: 800;
+ padding: 3px 10px;
+ border-radius: 20px;
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+}
+
+.dag-category-pill {
+ font-size: 0.75rem;
+ font-weight: 700;
+ padding: 3px 10px;
+ border-radius: 6px;
+ text-transform: uppercase;
+}
+
+.cat-gateway { background: rgba(56, 189, 248, 0.15); color: #38bdf8; }
+.cat-core { background: rgba(99, 102, 241, 0.15); color: #818cf8; }
+.cat-pattern { background: rgba(236, 72, 153, 0.15); color: #f472b6; }
+.cat-framework { background: rgba(245, 158, 11, 0.15); color: #f59e0b; }
+.cat-capstone { background: rgba(16, 185, 129, 0.15); color: #10b981; }
+
+.dag-status-indicator {
+ font-size: 0.8rem;
+ font-weight: 600;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.status-in-progress {
+ color: #38bdf8;
+ animation: pulseGlow 1.8s infinite;
+}
+
+.status-completed {
+ color: #10b981;
+}
+
+.status-locked {
+ color: var(--text-muted);
+}
+
+@keyframes pulseGlow {
+ 0%, 100% { opacity: 1; transform: scale(1); }
+ 50% { opacity: 0.7; transform: scale(0.98); }
+}
+
+/* Node Content */
+.dag-node-title {
+ font-size: 1.2rem;
+ font-weight: 700;
+ color: var(--text-color);
+ margin-bottom: 4px;
+}
+
+.dag-node-course-sub {
+ font-size: 0.84rem;
+ color: var(--accent-color);
+ margin-bottom: 12px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.dag-node-chips {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-bottom: 14px;
+}
+
+.dag-topic-tag {
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ font-size: 0.78rem;
+ padding: 3px 10px;
+ border-radius: 6px;
+ color: var(--text-muted);
+}
+
+[data-theme="light"] .dag-topic-tag {
+ background: rgba(0, 0, 0, 0.04);
+ border-color: rgba(0, 0, 0, 0.08);
+}
+
+.dag-project-box {
+ background: rgba(16, 185, 129, 0.08);
+ border: 1px dashed rgba(16, 185, 129, 0.3);
+ border-radius: 8px;
+ padding: 10px 14px;
+ font-size: 0.85rem;
+ color: #10b981;
+ margin-bottom: 14px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: 600;
+}
+
+.dag-node-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 10px;
+ padding-top: 12px;
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
+}
+
+.dag-hours-pill {
+ font-size: 0.82rem;
+ color: var(--text-muted);
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.dag-toggle-complete-btn {
+ background: transparent;
+ border: 1px solid var(--border-color);
+ color: var(--text-color);
+ padding: 6px 14px;
+ border-radius: 8px;
+ font-size: 0.82rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.dag-toggle-complete-btn:hover {
+ border-color: #10b981;
+ color: #10b981;
+}
+
+.dag-toggle-complete-btn.is-done {
+ background: #10b981;
+ border-color: #10b981;
+ color: #ffffff;
+}
+
+/* Connector Line Between DAG Nodes */
+.dag-connector {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 48px;
+ position: relative;
+}
+
+.dag-connector-line {
+ width: 3px;
+ height: 32px;
+ background: linear-gradient(180deg, var(--accent-color), #818cf8);
+ border-radius: 2px;
+}
+
+.dag-connector-arrow {
+ color: #818cf8;
+ font-size: 0.9rem;
+ margin-top: -6px;
+}
+
+.dag-connector-label {
+ position: absolute;
+ right: calc(50% + 12px);
+ background: rgba(15, 23, 42, 0.8);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ color: var(--text-muted);
+ font-size: 0.72rem;
+ padding: 2px 8px;
+ border-radius: 10px;
+ white-space: nowrap;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+[data-theme="light"] .dag-connector-label {
+ background: #ffffff;
+ border-color: rgba(0, 0, 0, 0.1);
+}
+
diff --git a/assistant.html b/assistant.html
new file mode 100644
index 0000000..7931a09
--- /dev/null
+++ b/assistant.html
@@ -0,0 +1,370 @@
+
+
+
+
+
+
+ AI Learning Assistant โ LearnSphere
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AI-Powered Curriculum Engine
+
+ Your Personalized Learning Assistant
+
+ Describe your technical ambition, current background, and timeframe.
+ Our AI searches LearnSphere's structured content library to build your custom milestone roadmap.
+
+
+
+
+
+
+ Roadmap Planner
+
+
+ Explore Content Catalog
+
+
+ Recent Guidance
+
+
+
+
+
+
+
+
+ What is your learning goal?
+
+
+
+
+ Quick-start goal prompts (click to try):
+
+
+
+ โก Basic JS to React Developer (3 Months)
+
+
+ ๐ Beginner to Machine Learning with Python (4 Months)
+
+
+ ๐ Frontend to Full-Stack Node & SQL (6 Weeks)
+
+
+ โ Java OOP & Spring Boot Microservices (8 Weeks)
+
+
+ ๐๏ธ SQL Database Architecture & Optimization (1 Month)
+
+
+
+
+
+
+ Current Skill Level
+
+ Auto-detect from description
+ Beginner (Starting from scratch)
+ Intermediate (Know fundamentals)
+ Advanced (Experienced developer)
+
+
+
+
+ Target Timeframe
+
+ 3 Months
+ 6 Weeks
+ 4 Months
+ 6 Months
+ Self-Paced (Flexible)
+
+
+
+
+ Weekly Commitment
+
+ ~6 hours/week (Part-time)
+ ~4 hours/week (Light)
+ ~10 hours/week (Standard)
+ ~15 hours/week (Accelerated)
+ ~25 hours/week (Intensive)
+
+
+
+
+
+
+ Tip: Press Ctrl+Enter to submit
+
+
+ Generate Learning Roadmap
+
+
+
+
+
+
+
+ Crafting Your Custom Learning Path
+ Analyzing your career ambition and skill profile...
+
+
+
+
+ Recommendation Error
+ Unable to generate recommendation.
+ Try Again
+
+
+
+
+
+ No Roadmap Generated Yet
+ Type your learning goals above or select one of the quick-start templates to see personalized recommendations in action.
+
+
+
+
+
+
+
+
+
+ Personalized Curriculum Overview
+
+
+
+ Why This Path Fits You (Strategic Breakdown)
+
+
+
+
+
+
+
+
+
+
+
Your Immediate Next Step for Today
+
+
+
+
+
+
+
+
+
+ Interactive Learning Roadmap (DAG Graph)
+
+
+
+
+
+ DAG Graph Flow
+
+
+ Step-by-Step Milestones
+
+
+
+
+
+ Technical topics are connected in a Directed Acyclic Graph (DAG) dependency sequence. Each node represents an actionable curriculum milestone with lessons, hours, and code deliverables.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Matched LearnSphere Courses & Structured Content
+
+
+
+
+
+
+
+
+ Alternative Pathways & Growth Options
+
+
+
+
+
+
+
+
+ Ask Follow-up Questions About Your Roadmap
+
+
+ Have questions about study pacing, interview preparation, or specific code exercises? Ask below:
+
+
+
+
+
+
+
+
+
+ Send
+
+
+
+
+
+
+
+
+
+
+ LearnSphere Technical Curriculum Catalog
+
+
+ Explore all 8 technical tracks created for LearnSphere. Click any course to have the AI build a custom roadmap around it!
+
+
+
+
+
+ All Topics
+ JavaScript
+ React
+ Node.js
+ Python
+ Java
+ SQL
+ Machine Learning
+ Full Stack
+
+
+
+ All Difficulties
+ Beginner
+ Intermediate
+ Advanced
+
+
+
+
+
+
+
+
+
+
+ Past Learning Queries & Recommendations
+
+
+ Review your previous guidance sessions stored in the LearnSphere database. Click any session to reload its plan.
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assistant.js b/assistant.js
new file mode 100644
index 0000000..723ec38
--- /dev/null
+++ b/assistant.js
@@ -0,0 +1,868 @@
+/**
+ * assistant.js โ LearnSphere AI Learning Assistant Client Controller
+ */
+
+(function () {
+ "use strict";
+
+ // API Base URL (proxied by Vite during dev or uses port 5000 directly)
+ const API_BASE = window.location.origin;
+
+ // DOM Elements
+ const promptInput = document.getElementById("goalPrompt");
+ const levelSelect = document.getElementById("learnerLevel");
+ const timeframeSelect = document.getElementById("targetTimeframe");
+ const hoursSelect = document.getElementById("weeklyHours");
+ const generateBtn = document.getElementById("generateBtn");
+
+ const loadingBox = document.getElementById("loadingBox");
+ const loadingTicker = document.getElementById("loadingTicker");
+ const errorBox = document.getElementById("errorBox");
+ const errorMessage = document.getElementById("errorMessage");
+ const emptyState = document.getElementById("emptyState");
+ const resultsWrapper = document.getElementById("resultsWrapper");
+
+ // Results Sub-containers
+ const profilePillsContainer = document.getElementById("profilePillsContainer");
+ const summaryText = document.getElementById("summaryText");
+ const whyRecommendedText = document.getElementById("whyRecommendedText");
+ const takeawaysGrid = document.getElementById("takeawaysGrid");
+ const nextStepDetailed = document.getElementById("nextStepDetailed");
+ const immediateNextStepText = document.getElementById("immediateNextStepText");
+ const dagFlowTree = document.getElementById("dagFlowTree");
+ const roadmapTimeline = document.getElementById("roadmapTimeline");
+ const dagToggleTree = document.getElementById("dagToggleTree");
+ const dagToggleTimeline = document.getElementById("dagToggleTimeline");
+ const recommendedCoursesGrid = document.getElementById("recommendedCoursesGrid");
+ const alternativePathsContainer = document.getElementById("alternativePathsContainer");
+
+ // Chat Elements
+ const chatContainer = document.getElementById("chatMessages");
+ const chatInput = document.getElementById("chatInput");
+ const chatSendBtn = document.getElementById("chatSendBtn");
+ const suggestedChipsContainer = document.getElementById("suggestedChips");
+
+ // Tab buttons & Views
+ const tabRoadmap = document.getElementById("tabRoadmap");
+ const tabCatalog = document.getElementById("tabCatalog");
+ const tabHistory = document.getElementById("tabHistory");
+ const assistantMainView = document.getElementById("assistantMainView");
+ const catalogView = document.getElementById("catalogView");
+ const historyView = document.getElementById("historyView");
+
+ // Catalog elements
+ const catalogSearchInput = document.getElementById("catalogSearch");
+ const catalogTopicFilter = document.getElementById("catalogTopicFilter");
+ const catalogDiffFilter = document.getElementById("catalogDiffFilter");
+ const allCoursesGrid = document.getElementById("allCoursesGrid");
+ const historyList = document.getElementById("historyList");
+
+ let currentSessionId = "sess-" + Math.random().toString(36).substring(2, 9);
+ let currentQueryId = null;
+ let allCoursesData = [];
+
+ // Loading Step Ticker Messages
+ const tickerSteps = [
+ "Analyzing your career ambition and skill profile...",
+ "Querying LearnSphere's structured content database...",
+ "Identifying prerequisite bridges and knowledge gaps...",
+ "Computing semantic relevance and course progression...",
+ "Synthesizing your personalized roadmap and immediate next step..."
+ ];
+ let tickerInterval = null;
+
+ // --- Tab Switching ---
+ function switchTab(activeTab) {
+ [tabRoadmap, tabCatalog, tabHistory].forEach(t => t && t.classList.remove("active"));
+ if (assistantMainView) assistantMainView.style.display = "none";
+ if (catalogView) catalogView.style.display = "none";
+ if (historyView) historyView.style.display = "none";
+
+ if (activeTab === "roadmap") {
+ if (tabRoadmap) tabRoadmap.classList.add("active");
+ if (assistantMainView) assistantMainView.style.display = "block";
+ } else if (activeTab === "catalog") {
+ if (tabCatalog) tabCatalog.classList.add("active");
+ if (catalogView) catalogView.style.display = "block";
+ loadCatalogContent();
+ } else if (activeTab === "history") {
+ if (tabHistory) tabHistory.classList.add("active");
+ if (historyView) historyView.style.display = "block";
+ loadHistoryData();
+ }
+ }
+
+ if (tabRoadmap) tabRoadmap.addEventListener("click", () => switchTab("roadmap"));
+ if (tabCatalog) tabCatalog.addEventListener("click", () => switchTab("catalog"));
+ if (tabHistory) tabHistory.addEventListener("click", () => switchTab("history"));
+
+ // --- DAG Graph vs Timeline Switcher ---
+ if (dagToggleTree && dagToggleTimeline && dagFlowTree && roadmapTimeline) {
+ dagToggleTree.addEventListener("click", () => {
+ dagToggleTree.classList.add("active");
+ dagToggleTimeline.classList.remove("active");
+ dagFlowTree.style.display = "flex";
+ roadmapTimeline.style.display = "none";
+ });
+
+ dagToggleTimeline.addEventListener("click", () => {
+ dagToggleTimeline.classList.add("active");
+ dagToggleTree.classList.remove("active");
+ dagFlowTree.style.display = "none";
+ roadmapTimeline.style.display = "block";
+ });
+ }
+
+ // --- Preset Prompt Chips ---
+ document.querySelectorAll(".chip-btn").forEach(btn => {
+ btn.addEventListener("click", () => {
+ const promptText = btn.getAttribute("data-prompt");
+ if (promptText && promptInput) {
+ promptInput.value = promptText;
+ promptInput.focus();
+ // Auto submit for rapid demo evaluation
+ submitGoalRequest();
+ }
+ });
+ });
+
+ // --- Start / Stop Loading ---
+ function startLoading() {
+ if (emptyState) emptyState.style.display = "none";
+ if (resultsWrapper) resultsWrapper.style.display = "none";
+ if (errorBox) errorBox.style.display = "none";
+ if (loadingBox) {
+ loadingBox.style.display = "block";
+ loadingBox.scrollIntoView({ behavior: "smooth", block: "center" });
+ }
+ if (generateBtn) {
+ generateBtn.disabled = true;
+ generateBtn.innerHTML = ` Generating Roadmap...`;
+ }
+
+ let stepIndex = 0;
+ if (loadingTicker) loadingTicker.textContent = tickerSteps[0];
+ if (tickerInterval) clearInterval(tickerInterval);
+ tickerInterval = setInterval(() => {
+ stepIndex = (stepIndex + 1) % tickerSteps.length;
+ if (loadingTicker) loadingTicker.textContent = tickerSteps[stepIndex];
+ }, 1200);
+ }
+
+ function stopLoading() {
+ if (tickerInterval) {
+ clearInterval(tickerInterval);
+ tickerInterval = null;
+ }
+ if (loadingBox) loadingBox.style.display = "none";
+ if (generateBtn) {
+ generateBtn.disabled = false;
+ generateBtn.innerHTML = ` Generate Learning Roadmap`;
+ }
+ }
+
+ function showError(msg) {
+ stopLoading();
+ if (errorMessage) errorMessage.textContent = msg || "An error occurred while generating your recommendation.";
+ if (errorBox) errorBox.style.display = "block";
+ if (resultsWrapper) resultsWrapper.style.display = "none";
+ }
+
+ // --- Submit Goal Request ---
+ async function submitGoalRequest() {
+ const query = promptInput ? promptInput.value.trim() : "";
+ if (!query) {
+ alert("Please describe your learning goal or current background.");
+ if (promptInput) promptInput.focus();
+ return;
+ }
+
+ const payload = {
+ query: query,
+ current_level: levelSelect ? levelSelect.value : "auto",
+ target_timeframe: timeframeSelect ? timeframeSelect.value : "3 months",
+ weekly_hours: hoursSelect ? parseInt(hoursSelect.value, 10) : 6
+ };
+
+ startLoading();
+
+ try {
+ const response = await fetch(`${API_BASE}/api/ai/recommend`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload)
+ });
+
+ if (!response.ok) {
+ const errData = await response.json().catch(() => ({}));
+ throw new Error(errData.detail || `Server returned status ${response.status}`);
+ }
+
+ const data = await response.json();
+ stopLoading();
+ renderRecommendations(data);
+ } catch (err) {
+ console.error("AI Recommendation error:", err);
+ showError(`Failed to generate recommendation: ${err.message}. Please check that the AI backend is active.`);
+ }
+ }
+
+ if (generateBtn) {
+ generateBtn.addEventListener("click", submitGoalRequest);
+ }
+
+ if (promptInput) {
+ promptInput.addEventListener("keydown", (e) => {
+ if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+ submitGoalRequest();
+ }
+ });
+ }
+
+ // --- Render Recommendations ---
+ function renderRecommendations(data) {
+ currentQueryId = data.query_id;
+
+ // 1. Learner Profile Pills
+ const p = data.learner_profile || {};
+ if (profilePillsContainer) {
+ profilePillsContainer.innerHTML = `
+ Goal: ${escapeHtml(p.target_role || "Developer")}
+ Detected Level: ${escapeHtml(p.current_level || "Beginner")}
+ Target Pace: ${escapeHtml(p.timeframe || "3 months")} (~${p.weekly_commitment_hours || 6} hrs/wk)
+ `;
+ }
+
+ // 2. Summary & Structured Takeaways
+ if (summaryText) summaryText.innerHTML = formatMarkdown(data.summary || "");
+ if (whyRecommendedText) whyRecommendedText.innerHTML = formatMarkdown(data.why_recommended || "");
+ renderTakeawaysGrid(data);
+
+ // 3. Immediate Next Step
+ renderNextStepDetailed(data);
+
+ // 4. Interactive DAG Graph Roadmap
+ renderDAGTree(data);
+
+ // 5. Learning Sequence / Roadmap Timeline (Alternative View)
+ if (roadmapTimeline) {
+ roadmapTimeline.innerHTML = "";
+ const seq = data.learning_sequence || [];
+ seq.forEach((phase, idx) => {
+ const phaseEl = document.createElement("div");
+ phaseEl.className = "timeline-phase";
+ phaseEl.innerHTML = `
+ ${phase.phase_number || (idx + 1)}
+
+
+
${escapeHtml(phase.weekly_focus || "")}
+
+ ${escapeHtml(phase.milestone_goal || "")}
+
+ ${phase.actionable_project ? `
+
+ Milestone Project: ${escapeHtml(phase.actionable_project)}
+
+ ` : ""}
+
+ `;
+ roadmapTimeline.appendChild(phaseEl);
+ });
+ }
+
+ // 5. Recommended Course Cards
+ recommendedCoursesGrid.innerHTML = "";
+ const courses = data.recommended_courses || [];
+ courses.forEach(c => {
+ const card = document.createElement("div");
+ card.className = "course-card";
+
+ const diffClass = c.difficulty.toLowerCase() === "beginner" ? "diff-beginner"
+ : (c.difficulty.toLowerCase() === "intermediate" ? "diff-intermediate" : "diff-advanced");
+
+ const resourceBadges = (c.resource_types || []).map(r => `${escapeHtml(r)} `).join("");
+ const topicsChips = (c.topics_covered || []).slice(0, 5).map(t => ` ${escapeHtml(t)} `).join("");
+
+ const modulesList = (c.learning_sequence || []).map(m => `
+
+
${escapeHtml(m.title)} (${escapeHtml(m.duration)})
+
${escapeHtml(m.hands_on_project || m.summary)}
+
+ `).join("");
+
+ card.innerHTML = `
+
+
+ ${escapeHtml(c.topic)}
+ ${escapeHtml(c.difficulty)}
+
+
${escapeHtml(c.title)}
+
+ ${escapeHtml(c.estimated_duration)}
+ ${c.total_hours} hrs total
+
+
${escapeHtml(c.description)}
+
+
+ Why Selected:
+
+
+ ${escapeHtml(c.why_relevant)}
+
+
+
Resource Formats:
+
${resourceBadges}
+
+
Key Topics Covered:
+
${topicsChips}
+
+
+ View Curriculum Modules (${(c.learning_sequence || []).length})
+
+
+
${modulesList}
+
+ ${c.capstone_project && c.capstone_project.title ? `
+
+ Capstone: ${escapeHtml(c.capstone_project.title)} โ ${escapeHtml(c.capstone_project.description)}
+
+ ` : ""}
+
+
+
+
+ Start Course
+
+
+ `;
+
+ // Toggle modules accordion
+ const toggleBtn = card.querySelector(".modules-toggle");
+ const accordion = card.querySelector(".modules-accordion");
+ const arrow = card.querySelector(".toggle-arrow");
+ toggleBtn.addEventListener("click", () => {
+ const isOpen = accordion.style.display === "block";
+ accordion.style.display = isOpen ? "none" : "block";
+ arrow.style.transform = isOpen ? "rotate(0deg)" : "rotate(180deg)";
+ });
+
+ // Start course action
+ const startBtn = card.querySelector(".start-course-btn");
+ startBtn.addEventListener("click", () => {
+ alert(`Starting course: ${c.title}! Modules and interactive practice labs are now active in your learner queue.`);
+ });
+
+ recommendedCoursesGrid.appendChild(card);
+ });
+
+ // 6. Alternative Pathways
+ if (alternativePathsContainer) {
+ const alts = data.alternative_paths || [];
+ if (alts.length > 0) {
+ alternativePathsContainer.innerHTML = alts.map(a => ` ${escapeHtml(a)} `).join("");
+ alternativePathsContainer.parentElement.style.display = "block";
+ } else {
+ alternativePathsContainer.parentElement.style.display = "none";
+ }
+ }
+
+ // Initialize Chat greeting
+ initChatWithGreeting(data);
+
+ // Display results
+ if (resultsWrapper) {
+ resultsWrapper.style.display = "block";
+ resultsWrapper.scrollIntoView({ behavior: "smooth" });
+ }
+ }
+
+ // --- Render Structured Takeaways Grid (No Monolithic Paragraphs) ---
+ function renderTakeawaysGrid(data) {
+ if (!takeawaysGrid) return;
+ takeawaysGrid.innerHTML = "";
+
+ const points = data.why_recommended_points || [];
+ if (points.length > 0) {
+ takeawaysGrid.innerHTML = points.map(pt => `
+
+
+
${escapeHtml(pt.description || "")}
+
+ `).join("");
+ } else if (data.why_recommended) {
+ takeawaysGrid.innerHTML = `
+
+
+
${formatMarkdown(data.why_recommended)}
+
+ `;
+ }
+ }
+
+ // --- Render Immediate Next Step Detail Card ---
+ function renderNextStepDetailed(data) {
+ if (!immediateNextStepText) return;
+ const ns = data.next_step_details;
+ const actionTitle = ns ? (ns.action_item || ns.action_title || ns.module_title) : null;
+ if (ns && actionTitle) {
+ const course = ns.course_title || ns.course_name || "Primary Course";
+ const estTime = ns.estimated_time || (ns.estimated_hours ? `~${ns.estimated_hours} Hours` : "1-2 Hours");
+ const deliverable = ns.hands_on_project || ns.deliverable || "";
+ immediateNextStepText.innerHTML = `
+
+ ${escapeHtml(actionTitle)}
+
+
+ Target Course: ${escapeHtml(course)}
+ Est. Time: ${escapeHtml(estTime)}
+
+ ${deliverable ? `
+
+
Concrete Action Deliverable:
+
${escapeHtml(deliverable)}
+
+ ` : ""}
+ `;
+ } else {
+ immediateNextStepText.innerHTML = formatMarkdown(data.immediate_next_step || "");
+ }
+ }
+
+ // --- Render DAG Graph Roadmap Flow (Interactive Directed Acyclic Graph) ---
+ function renderDAGTree(data) {
+ if (!dagFlowTree) return;
+ dagFlowTree.innerHTML = "";
+
+ let nodes = (data.dag_graph && data.dag_graph.nodes) ? data.dag_graph.nodes : [];
+
+ // Fallback: If no DAG graph nodes in payload, derive from primary course or sequence
+ if (!nodes || nodes.length === 0) {
+ if (data.recommended_courses && data.recommended_courses.length > 0 && data.recommended_courses[0].learning_sequence) {
+ const course = data.recommended_courses[0];
+ nodes = course.learning_sequence.map((mod, idx) => ({
+ id: `node-${idx + 1}`,
+ step_number: idx + 1,
+ title: mod.title,
+ category: idx === 0 ? "Foundational Gateway" : (idx === course.learning_sequence.length - 1 ? "Capstone Milestone" : "Core Architecture"),
+ duration: mod.duration || "1-2 weeks",
+ estimated_hours: 6,
+ summary: mod.summary || "",
+ hands_on_project: mod.hands_on_project || "",
+ skills_acquired: mod.topics || [],
+ prerequisites: idx > 0 ? [`node-${idx}`] : [],
+ unlocks: idx < course.learning_sequence.length - 1 ? [`node-${idx + 2}`] : [],
+ status: idx === 0 ? "in_progress" : "unlocked"
+ }));
+ } else if (data.learning_sequence && data.learning_sequence.length > 0) {
+ nodes = data.learning_sequence.map((phase, idx) => ({
+ id: `node-${idx + 1}`,
+ step_number: phase.phase_number || (idx + 1),
+ title: phase.phase_title,
+ category: idx === 0 ? "Foundational Gateway" : (idx === data.learning_sequence.length - 1 ? "Capstone Milestone" : "Core Architecture"),
+ duration: `~${phase.duration_weeks || 2} weeks`,
+ estimated_hours: (phase.duration_weeks || 2) * 6,
+ summary: phase.weekly_focus || "",
+ hands_on_project: phase.actionable_project || phase.milestone_goal || "",
+ skills_acquired: [phase.milestone_goal || "Core Competency"],
+ prerequisites: idx > 0 ? [`node-${idx}`] : [],
+ unlocks: idx < data.learning_sequence.length - 1 ? [`node-${idx + 2}`] : [],
+ status: idx === 0 ? "in_progress" : "unlocked"
+ }));
+ }
+ }
+
+ if (!nodes || nodes.length === 0) {
+ dagFlowTree.innerHTML = `No roadmap nodes available.
`;
+ return;
+ }
+
+ const totalNodes = nodes.length;
+
+ // Top progress bar header
+ const progressHeader = document.createElement("div");
+ progressHeader.className = "dag-progress-header";
+ progressHeader.style.cssText = "width:100%; max-width:820px; background:var(--card-bg); border:1px solid var(--card-border); border-radius:14px; padding:16px 22px; margin-bottom:28px; display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:14px; box-shadow:var(--card-shadow);";
+
+ progressHeader.innerHTML = `
+
+
+
+
+
+
DAG Curriculum Mastery Track
+
0 of ${totalNodes} milestones completed (0%)
+
+
+
+ `;
+ dagFlowTree.appendChild(progressHeader);
+
+ const completedSet = new Set();
+
+ function updateProgress() {
+ const count = completedSet.size;
+ const pct = Math.round((count / totalNodes) * 100);
+ const bar = document.getElementById("dagProgressBar");
+ const txt = document.getElementById("dagProgressText");
+ if (bar) bar.style.width = `${pct}%`;
+ if (txt) txt.textContent = `${count} of ${totalNodes} milestones completed (${pct}%)`;
+ }
+
+ // Render nodes & directed connectors
+ nodes.forEach((node, idx) => {
+ const card = document.createElement("div");
+ card.className = "dag-node-card";
+ card.id = `dag-card-${node.id}`;
+
+ const stepNum = node.phase || node.step_number || (typeof node.level === "number" ? node.level + 1 : idx + 1);
+ const durationStr = node.duration_label || node.duration || "1-2 weeks";
+ const topics = node.key_topics || node.skills_acquired || [];
+
+ // Category style class
+ let catClass = "cat-core";
+ const cat = (node.category || "").toLowerCase();
+ if (cat.includes("gateway") || cat.includes("foundation")) catClass = "cat-gateway";
+ else if (cat.includes("pattern") || cat.includes("advanced")) catClass = "cat-pattern";
+ else if (cat.includes("framework") || cat.includes("backend")) catClass = "cat-framework";
+ else if (cat.includes("capstone") || cat.includes("milestone")) catClass = "cat-capstone";
+
+ const skillsHtml = topics.map(s => `
+ ${escapeHtml(s)}
+ `).join("");
+
+ const isFirst = idx === 0;
+ const initialStatus = isFirst ? "Current Milestone" : (idx === totalNodes - 1 ? "Capstone Goal" : "Prerequisite Locked");
+ const statusIcon = isFirst ? "fa-solid fa-bolt" : (idx === totalNodes - 1 ? "fa-solid fa-flag-checkered" : "fa-solid fa-arrow-down");
+ const statusClass = isFirst ? "status-in-progress" : "";
+
+ card.innerHTML = `
+
+
+ ${escapeHtml(node.title)}
+ ${node.course_title ? ` ${escapeHtml(node.course_title)}
` : ""}
+ ${node.summary ? `${escapeHtml(node.summary)}
` : ""}
+
+ ${skillsHtml ? `
+ Skills Acquired:
+ ${skillsHtml}
+ ` : ""}
+
+ ${node.hands_on_project ? `
+
+
+
+ Hands-on Project:
+ ${escapeHtml(node.hands_on_project)}
+
+
+ ` : ""}
+
+
+ `;
+
+ // Interactive completion toggle
+ const btn = card.querySelector(".dag-toggle-complete-btn");
+ btn.addEventListener("click", () => {
+ const nodeId = node.id;
+ const statusEl = card.querySelector(`#dag-status-${nodeId}`);
+ if (completedSet.has(nodeId)) {
+ completedSet.delete(nodeId);
+ btn.classList.remove("is-done");
+ btn.innerHTML = ` Mark Complete `;
+ card.style.borderColor = "";
+ card.style.background = "";
+ if (statusEl) {
+ statusEl.className = `dag-status-indicator ${isFirst ? "status-in-progress" : ""}`;
+ statusEl.innerHTML = ` ${initialStatus} `;
+ }
+ } else {
+ completedSet.add(nodeId);
+ btn.classList.add("is-done");
+ btn.innerHTML = ` Milestone Completed! `;
+ card.style.borderColor = "rgba(16, 185, 129, 0.5)";
+ card.style.background = "linear-gradient(135deg, var(--card-bg) 80%, rgba(16, 185, 129, 0.08))";
+ if (statusEl) {
+ statusEl.className = "dag-status-indicator status-completed";
+ statusEl.innerHTML = ` Completed `;
+ }
+ }
+ updateProgress();
+ });
+
+ dagFlowTree.appendChild(card);
+
+ // Render connector to next milestone if not the final node
+ if (idx < totalNodes - 1) {
+ const connector = document.createElement("div");
+ connector.className = "dag-connector";
+ connector.innerHTML = `
+ unlocks milestone #${idx + 2}
+
+
+ `;
+ dagFlowTree.appendChild(connector);
+ }
+ });
+ }
+
+ // --- Interactive Follow-up Chat ---
+ function initChatWithGreeting(data) {
+ if (!chatContainer) return;
+ chatContainer.innerHTML = `
+
+ Hello! I have created your personalized ${escapeHtml(data.learner_profile.target_role || "learning")} roadmap.
+ You can ask me anything about your pace, how to prepare for interviews, or how to tackle specific modules!
+
+ `;
+ }
+
+ async function sendChatMessage() {
+ const msg = chatInput ? chatInput.value.trim() : "";
+ if (!msg) return;
+
+ // Append user bubble
+ appendChatBubble("user", msg);
+ if (chatInput) chatInput.value = "";
+
+ // Typing indicator
+ const typingEl = document.createElement("div");
+ typingEl.className = "chat-bubble assistant";
+ typingEl.id = "chatTypingIndicator";
+ typingEl.innerHTML = ` Thinking...`;
+ chatContainer.appendChild(typingEl);
+ chatContainer.scrollTop = chatContainer.scrollHeight;
+
+ try {
+ const res = await fetch(`${API_BASE}/api/ai/chat`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ session_id: currentSessionId,
+ message: msg,
+ context_query_id: currentQueryId
+ })
+ });
+
+ const typing = document.getElementById("chatTypingIndicator");
+ if (typing) typing.remove();
+
+ if (!res.ok) throw new Error("Could not reach chat server");
+ const data = await res.json();
+ appendChatBubble("assistant", data.reply);
+
+ // Render suggested prompt chips if available
+ if (suggestedChipsContainer && data.suggested_prompts) {
+ suggestedChipsContainer.innerHTML = data.suggested_prompts.map(p => `
+ ${escapeHtml(p)}
+ `).join("");
+
+ suggestedChipsContainer.querySelectorAll(".chat-suggestion-chip").forEach(btn => {
+ btn.addEventListener("click", () => {
+ const nextMsg = btn.getAttribute("data-msg");
+ if (chatInput) chatInput.value = nextMsg;
+ sendChatMessage();
+ });
+ });
+ }
+ } catch (err) {
+ const typing = document.getElementById("chatTypingIndicator");
+ if (typing) typing.remove();
+ appendChatBubble("assistant", "Sorry, I encountered an issue replying. Please try again!");
+ }
+ }
+
+ function appendChatBubble(role, text) {
+ if (!chatContainer) return;
+ const bubble = document.createElement("div");
+ bubble.className = `chat-bubble ${role}`;
+ bubble.innerHTML = formatMarkdown(text);
+ chatContainer.appendChild(bubble);
+ chatContainer.scrollTop = chatContainer.scrollHeight;
+ }
+
+ if (chatSendBtn) chatSendBtn.addEventListener("click", sendChatMessage);
+ if (chatInput) {
+ chatInput.addEventListener("keydown", (e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ sendChatMessage();
+ }
+ });
+ }
+
+ // --- Content Catalog Explorer ---
+ async function loadCatalogContent() {
+ if (!allCoursesGrid) return;
+ allCoursesGrid.innerHTML = `Loading content catalog...
`;
+
+ try {
+ const res = await fetch(`${API_BASE}/api/ai/content`);
+ const data = await res.json();
+ allCoursesData = data.courses || [];
+ renderCatalogList(allCoursesData);
+ } catch (err) {
+ allCoursesGrid.innerHTML = `Failed to load catalog. Please ensure the backend server is running.
`;
+ }
+ }
+
+ function renderCatalogList(courses) {
+ if (!allCoursesGrid) return;
+ allCoursesGrid.innerHTML = "";
+
+ if (courses.length === 0) {
+ allCoursesGrid.innerHTML = `No courses found matching your filter criteria.
`;
+ return;
+ }
+
+ courses.forEach(c => {
+ const card = document.createElement("div");
+ card.className = "course-card";
+ const diffClass = c.difficulty.toLowerCase() === "beginner" ? "diff-beginner"
+ : (c.difficulty.toLowerCase() === "intermediate" ? "diff-intermediate" : "diff-advanced");
+
+ card.innerHTML = `
+
+
+ ${escapeHtml(c.topic)}
+ ${escapeHtml(c.difficulty)}
+
+
${escapeHtml(c.title)}
+
+ ${escapeHtml(c.estimated_duration)}
+ ${c.total_hours} hrs
+
+
${escapeHtml(c.description)}
+
+ Prerequisites: ${(c.prerequisites || []).join(", ") || "None"}
+
+
+
+ Plan Learning Path with this Course
+
+ `;
+
+ card.querySelector(".plan-with-course-btn").addEventListener("click", () => {
+ switchTab("roadmap");
+ if (promptInput) {
+ promptInput.value = `I want to master ${c.topic} and build real-world projects with ${c.title}.`;
+ submitGoalRequest();
+ }
+ });
+
+ allCoursesGrid.appendChild(card);
+ });
+ }
+
+ // Filter listeners for Catalog
+ function filterCatalog() {
+ const query = (catalogSearchInput ? catalogSearchInput.value : "").toLowerCase();
+ const topic = catalogTopicFilter ? catalogTopicFilter.value.toLowerCase() : "all";
+ const diff = catalogDiffFilter ? catalogDiffFilter.value.toLowerCase() : "all";
+
+ const filtered = allCoursesData.filter(c => {
+ const matchesQuery = !query || c.title.toLowerCase().includes(query) || c.description.toLowerCase().includes(query) || (c.topics_covered || []).some(t => t.toLowerCase().includes(query));
+ const matchesTopic = topic === "all" || c.topic.toLowerCase() === topic;
+ const matchesDiff = diff === "all" || c.difficulty.toLowerCase() === diff;
+ return matchesQuery && matchesTopic && matchesDiff;
+ });
+
+ renderCatalogList(filtered);
+ }
+
+ if (catalogSearchInput) catalogSearchInput.addEventListener("input", filterCatalog);
+ if (catalogTopicFilter) catalogTopicFilter.addEventListener("change", filterCatalog);
+ if (catalogDiffFilter) catalogDiffFilter.addEventListener("change", filterCatalog);
+
+ // --- Load History Data ---
+ async function loadHistoryData() {
+ if (!historyList) return;
+ historyList.innerHTML = `Loading history...
`;
+
+ try {
+ const res = await fetch(`${API_BASE}/api/ai/history?limit=10`);
+ const data = await res.json();
+ const queries = data.history || [];
+
+ if (queries.length === 0) {
+ historyList.innerHTML = `No past recommendations found yet. Try asking the assistant!
`;
+ return;
+ }
+
+ historyList.innerHTML = "";
+ queries.forEach(q => {
+ const item = document.createElement("div");
+ item.className = "history-item";
+ const dateStr = q.created_at ? new Date(q.created_at).toLocaleString() : "";
+ item.innerHTML = `
+ "${escapeHtml(q.query_text)}"
+ Role: ${escapeHtml(q.parsed_target_role || "Developer")} | Level: ${escapeHtml(q.parsed_current_level || "Beginner")} | ${dateStr}
+ `;
+ item.addEventListener("click", () => {
+ switchTab("roadmap");
+ if (promptInput) {
+ promptInput.value = q.query_text;
+ submitGoalRequest();
+ }
+ });
+ historyList.appendChild(item);
+ });
+ } catch (err) {
+ historyList.innerHTML = `Failed to load history.
`;
+ }
+ }
+
+ // --- Utility Helpers ---
+ function escapeHtml(str) {
+ if (!str) return "";
+ return String(str)
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'");
+ }
+
+ function formatMarkdown(str) {
+ if (!str) return "";
+ let formatted = escapeHtml(str);
+ // Bold: **text**
+ formatted = formatted.replace(/\*\*(.*?)\*\*/g, "$1 ");
+ // Italic: *text*
+ formatted = formatted.replace(/\*(.*?)\*/g, "$1 ");
+ // Line breaks
+ formatted = formatted.replace(/\n\n/g, "").replace(/\n/g, " ");
+ return `
${formatted}
`;
+ }
+
+})();
diff --git a/backend/__init__.py b/backend/__init__.py
new file mode 100644
index 0000000..33a2781
--- /dev/null
+++ b/backend/__init__.py
@@ -0,0 +1 @@
+# LearnSphere AI Backend Package
diff --git a/backend/ai_engine.py b/backend/ai_engine.py
new file mode 100644
index 0000000..c230fee
--- /dev/null
+++ b/backend/ai_engine.py
@@ -0,0 +1,852 @@
+import json
+import os
+import re
+import uuid
+from datetime import datetime, timezone
+from typing import List, Dict, Any, Optional, Tuple
+
+from dotenv import load_dotenv
+
+from backend.database import (
+ get_all_courses,
+ get_course_by_id,
+ save_user_query,
+ save_chat_message,
+ get_chat_history,
+)
+from backend.models import (
+ GoalRequest,
+ RecommendationResponse,
+ ParsedProfile,
+ RecommendedCourse,
+ LearningPhase,
+ ChatRequest,
+ ChatResponse,
+ DAGNode,
+ DAGEdge,
+ DAGGraph,
+ KeyTakeaway,
+ NextStepDetails,
+)
+
+load_dotenv()
+
+# Check for Gemini API key
+GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
+GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
+gemini_client = None
+
+if GEMINI_API_KEY and GEMINI_API_KEY != "your_gemini_api_key_here":
+ try:
+ from google import genai
+ gemini_client = genai.Client(api_key=GEMINI_API_KEY)
+ print(f"[AI ENGINE] Successfully initialized Google Gemini Client with model {GEMINI_MODEL}")
+ except Exception as e:
+ print(f"[AI ENGINE] Could not initialize Google GenAI: {e}. Using deterministic semantic engine.")
+
+
+# --- Semantic Similarity / TF-IDF Vectorizer helper ---
+def compute_semantic_relevance(query: str, courses: List[Dict[str, Any]]) -> List[Tuple[Dict[str, Any], float]]:
+ """
+ Computes semantic similarity scores between the user query and the course catalog
+ using TF-IDF term-frequency and keyword domain matching.
+ """
+ try:
+ from sklearn.feature_extraction.text import TfidfVectorizer
+ from sklearn.metrics.pairwise import cosine_similarity
+
+ # Build document corpus for each course
+ docs = []
+ for c in courses:
+ doc = (
+ f"{c['title']} {c['topic']} {c['difficulty']} "
+ f"{c['description']} {' '.join(c.get('topics_covered', []))} "
+ f"{' '.join(c.get('target_roles', []))} {' '.join(c.get('key_skills_acquired', []))}"
+ )
+ docs.append(doc)
+
+ vectorizer = TfidfVectorizer(stop_words="english", ngram_range=(1, 2))
+ tfidf_matrix = vectorizer.fit_transform(docs)
+ query_vec = vectorizer.transform([query])
+ sim_scores = cosine_similarity(query_vec, tfidf_matrix)[0]
+
+ scored = []
+ for idx, course in enumerate(courses):
+ base_score = float(sim_scores[idx])
+
+ # Heuristic domain boosts
+ query_lower = query.lower()
+ topic_lower = course["topic"].lower()
+ title_lower = course["title"].lower()
+
+ bonus = 0.0
+ # Java vs JavaScript boundary check
+ clean_query = query_lower.replace("javascript", " ") if "javascript" in query_lower else query_lower
+ if course["id"] == "course-java-springboot":
+ if re.search(r"\bjava\b", clean_query):
+ bonus += 0.40
+ else:
+ if re.search(rf"\b{re.escape(topic_lower)}\b", query_lower):
+ bonus += 0.40
+
+ if any(term in query_lower for term in [course["slug"]]):
+ bonus += 0.25
+
+ # Role matching bonus
+ for role in course.get("target_roles", []):
+ if re.search(rf"\b{re.escape(role.lower())}\b", query_lower):
+ bonus += 0.35
+
+ final_score = min(0.99, base_score + bonus)
+ scored.append((course, final_score))
+
+ scored.sort(key=lambda x: x[1], reverse=True)
+ return scored
+ except Exception as e:
+ print(f"[AI ENGINE] Fallback in TF-IDF calculation: {e}")
+ # Fallback keyword scoring
+ query_tokens = set(re.findall(r"\w+", query.lower()))
+ scored = []
+ for c in courses:
+ text = f"{c['title']} {c['topic']} {c['description']} {' '.join(c.get('topics_covered', []))}".lower()
+ words = set(re.findall(r"\w+", text))
+ common = len(query_tokens.intersection(words))
+ score = common / max(1, len(query_tokens))
+ scored.append((c, score))
+ scored.sort(key=lambda x: x[1], reverse=True)
+ return scored
+
+
+def parse_learner_profile(query: str, explicit_level: Optional[str] = None, explicit_timeframe: Optional[str] = None) -> ParsedProfile:
+ """
+ Extracts current level, target role, timeframe, and weekly study hours from natural language query.
+ """
+ q = query.lower()
+
+ # Determine Current Level
+ detected_level = "Beginner"
+ if explicit_level and explicit_level.lower() != "auto":
+ detected_level = explicit_level.capitalize()
+ else:
+ if any(term in q for term in ["advanced", "experienced", "production", "senior", "lead developer"]):
+ detected_level = "Advanced"
+ elif any(term in q for term in ["intermediate", "know basic", "knows basic", "some experience", "have built", "familiar with", "worked with"]):
+ detected_level = "Intermediate"
+ elif any(term in q for term in ["beginner", "starting out", "scratch", "new to", "no experience", "zero"]):
+ detected_level = "Beginner"
+
+ # Determine Target Role / Ambition
+ target_role = "Software Developer"
+ if any(k in q for k in ["react developer", "frontend developer", "frontend engineer", "front end", "react"]):
+ target_role = "React Frontend Developer"
+ elif any(k in q for k in ["full stack", "fullstack", "mern", "web developer"]):
+ target_role = "Full Stack Web Developer"
+ elif any(k in q for k in ["node", "backend", "api developer", "backend engineer"]):
+ target_role = "Backend Node.js Engineer"
+ elif any(k in q for k in ["machine learning", "ml engineer", "data scientist", "ai engineer"]):
+ target_role = "Machine Learning Engineer"
+ elif any(k in q for k in ["python developer", "data analyst", "python"]):
+ target_role = "Python Software Engineer"
+ elif re.search(r"\bjava\b", q.replace("javascript", " ")):
+ target_role = "Java Enterprise Backend Engineer"
+ elif any(k in q for k in ["sql", "database administrator", "dba", "data engineer"]):
+ target_role = "Database & SQL Specialist"
+
+ # Determine Timeframe
+ timeframe = explicit_timeframe if explicit_timeframe else "3 months"
+ match = re.search(r"(\d+)\s*(months?|weeks?|years?|days?)", q)
+ if match:
+ timeframe = f"{match.group(1)} {match.group(2)}"
+
+ # Determine Weekly hours
+ weekly_hours = 6
+ hour_match = re.search(r"(\d+)\s*(hours|hrs|hr)\s*(per|\/|a)?\s*week", q)
+ if hour_match:
+ weekly_hours = int(hour_match.group(1))
+
+ # Identify primary skills needed
+ skills = []
+ if "react" in q or "frontend" in target_role.lower():
+ skills.extend(["Modern JavaScript ES6+", "React Hooks", "State Management", "Component Composition", "REST API Integration"])
+ if "node" in q or "backend" in target_role.lower():
+ skills.extend(["Node.js Runtime", "Express APIs", "JWT Authentication", "Database Architecture"])
+ if "python" in q:
+ skills.extend(["Python OOP", "Data Structures", "Scripting", "Packaging"])
+ if "machine learning" in q or "data scientist" in target_role.lower():
+ skills.extend(["NumPy & Pandas", "Scikit-Learn", "Model Training", "Model Evaluation", "FastAPI Serving"])
+ if re.search(r"\bjava\b", q.replace("javascript", " ")):
+ skills.extend(["Core Java & JVM", "OOP Principles", "Java Collections", "Spring Boot 3 REST"])
+ if "sql" in q or "database" in q:
+ skills.extend(["Relational Schema Design", "Complex Joins", "Aggregations & Window Functions", "Query Optimization"])
+
+ if not skills:
+ skills = ["Foundational Syntax", "Data Structures", "Building Applied Projects", "Version Control"]
+
+ # Prerequisites to bridge
+ bridges = []
+ if "react" in q and ("basic javascript" in q or "beginner" in detected_level.lower()):
+ bridges.append("Solidify Asynchronous JavaScript (Promises, async/await, Array methods) before React")
+ if "machine learning" in q and "python" not in q:
+ bridges.append("Ensure Python programming fundamentals are solid before mathematical modeling")
+ if "full stack" in q:
+ bridges.append("Review Frontend React and Backend Express independently before combining")
+
+ return ParsedProfile(
+ current_level=detected_level,
+ target_role=target_role,
+ timeframe=timeframe,
+ weekly_commitment_hours=weekly_hours,
+ primary_skills_needed=skills,
+ prerequisites_to_bridge=bridges,
+ )
+
+
+def build_dag_graph(ordered_courses: List[Dict[str, Any]], profile: ParsedProfile) -> DAGGraph:
+ """
+ Constructs a Directed Acyclic Graph (DAG) representing the complete technical
+ skill dependency tree across all recommended modules and courses.
+ """
+ nodes: List[DAGNode] = []
+ edges: List[DAGEdge] = []
+
+ level = 0
+ prev_node_id = None
+
+ for c_idx, course in enumerate(ordered_courses):
+ phase_num = c_idx + 1
+ modules = course.get("learning_sequence", [])
+ total_mods = len(modules)
+
+ for m_idx, mod in enumerate(modules):
+ node_id = f"dag-{course['slug']}-m{m_idx+1}"
+
+ # Categorize the node cleanly
+ category = "Core Architecture"
+ if level == 0 and (course.get("difficulty", "").lower() == "beginner" or m_idx == 0):
+ category = "Foundational Gateway"
+ elif m_idx == total_mods - 1:
+ category = "Capstone Milestone"
+ elif any(k in mod.get("title", "").lower() for k in ["spring", "react", "express", "fastapi"]):
+ category = "Enterprise Framework"
+ elif any(k in mod.get("title", "").lower() for k in ["stream", "jpa", "auth", "advanced", "tuning", "window"]):
+ category = "Advanced Pattern"
+
+ # Parse hours from duration string (e.g., "1 week (6 hours)" -> 6)
+ hours = 6
+ h_match = re.search(r"(\d+)\s*(hours|hrs)", mod.get("duration", ""))
+ if h_match:
+ hours = int(h_match.group(1))
+
+ status = "in_progress" if level == 0 else "pending"
+ prereqs = [prev_node_id] if prev_node_id else []
+
+ node = DAGNode(
+ id=node_id,
+ title=mod.get("title", f"Module {m_idx+1}"),
+ level=level,
+ phase=phase_num,
+ category=category,
+ estimated_hours=hours,
+ duration_label=mod.get("duration", "1 week"),
+ course_id=course["id"],
+ course_title=course["title"],
+ prerequisites=prereqs,
+ key_topics=mod.get("topics", []),
+ hands_on_project=mod.get("hands_on_project"),
+ status=status
+ )
+ nodes.append(node)
+
+ if prev_node_id:
+ edges.append(DAGEdge(
+ from_node=prev_node_id,
+ to_node=node_id,
+ label="unlocks"
+ ))
+
+ prev_node_id = node_id
+ level += 1
+
+ return DAGGraph(nodes=nodes, edges=edges)
+
+
+def build_why_recommended_points(ordered_courses: List[Dict[str, Any]], profile: ParsedProfile) -> List[KeyTakeaway]:
+ points = []
+
+ # 1. Target Role Fit
+ points.append(KeyTakeaway(
+ title="Target Role Alignment",
+ description=f"Curated specifically for becoming a {profile.target_role}. Bridges your current level ({profile.current_level}) directly to production enterprise competence without unrelated topics.",
+ icon="fa-bullseye"
+ ))
+
+ # 2. Pacing & Feasibility
+ total_hours = sum(c.get("total_hours", 0) for c in ordered_courses)
+ weeks = max(2, round(total_hours / max(4, profile.weekly_commitment_hours)))
+ points.append(KeyTakeaway(
+ title="Pacing & Schedule Feasibility",
+ description=f"At your committed pace of ~{profile.weekly_commitment_hours} hrs/week, you will complete {total_hours} total curriculum hours across ~{weeks} weeks, fitting your {profile.timeframe} target.",
+ icon="fa-clock"
+ ))
+
+ # 3. Prerequisite Bridge
+ first_c = ordered_courses[0]
+ points.append(KeyTakeaway(
+ title="Grounded Prerequisite Bridge",
+ description=f"Solidifies foundational syntax and design patterns in '{first_c['title']}' before introducing framework complexity, avoiding common junior knowledge gaps.",
+ icon="fa-bridge"
+ ))
+
+ # 4. Capstone Portfolio Impact
+ last_c = ordered_courses[-1]
+ capstone_name = last_c.get("capstone_project", {}).get("title", "Production Microservice")
+ points.append(KeyTakeaway(
+ title="Verifiable Portfolio Artifact",
+ description=f"Culminates with delivering '{capstone_name}', a resume-ready project featuring persistence, testing, and clean code that recruiters look for in technical interviews.",
+ icon="fa-trophy"
+ ))
+
+ return points
+
+
+def build_next_step_details(first_course: Dict[str, Any], profile: ParsedProfile) -> NextStepDetails:
+ first_module = first_course.get("learning_sequence", [{}])[0]
+ return NextStepDetails(
+ course_title=first_course["title"],
+ module_title=first_module.get("title", "Module 1"),
+ action_item=first_module.get("summary", "Master the foundational environment and code syntax"),
+ hands_on_project=first_module.get("hands_on_project", "Build initial interactive practice exercise"),
+ estimated_time=first_module.get("duration", "Week 1 (6 hours)")
+ )
+
+
+def generate_heuristic_recommendation(
+ goal_req: GoalRequest,
+ courses: List[Dict[str, Any]],
+ scored_courses: List[Tuple[Dict[str, Any], float]],
+ profile: ParsedProfile,
+) -> RecommendationResponse:
+ """
+ Deterministic rule-based & semantic engine that selects courses,
+ orders them by dependency sequence, and generates tailored guidance.
+ """
+ query_id = f"ls-query-{uuid.uuid4().hex[:8]}"
+ query_lower = goal_req.query.lower()
+
+ # Filter top relevant courses (threshold score > 0.08 or top 3)
+ relevant_scored = [sc for sc in scored_courses if sc[1] > 0.08]
+ if not relevant_scored:
+ relevant_scored = scored_courses[:3]
+
+ selected_courses_map = {}
+ for c, score in relevant_scored[:4]:
+ selected_courses_map[c["id"]] = (c, score)
+
+ # Prerequisite chaining logic:
+ # 1. If React is selected and user knows only "basic javascript" or is beginner:
+ if "course-react-engineering" in selected_courses_map:
+ if ("basic" in query_lower or "beginner" in profile.current_level.lower()) and "course-js-core" not in selected_courses_map:
+ js_course = get_course_by_id("course-js-core")
+ if js_course:
+ selected_courses_map["course-js-core"] = (js_course, 0.85)
+
+ # 2. If Machine Learning is selected and user is beginner or hasn't mastered Python:
+ if "course-ml-foundations" in selected_courses_map:
+ if "course-python-mastery" not in selected_courses_map:
+ py_course = get_course_by_id("course-python-mastery")
+ if py_course:
+ selected_courses_map["course-python-mastery"] = (py_course, 0.88)
+
+ # 3. If Full-stack is requested, ensure backend and frontend courses are present
+ if "fullstack" in query_lower or "full stack" in query_lower:
+ for cid in ["course-react-engineering", "course-nodejs-backend", "course-sql-database"]:
+ if cid not in selected_courses_map:
+ c = get_course_by_id(cid)
+ if c:
+ selected_courses_map[cid] = (c, 0.75)
+
+ # Order courses logically:
+ # Order hierarchy: Beginner foundations -> Intermediate core -> Specialized -> Capstone
+ difficulty_order = {"Beginner": 1, "Intermediate": 2, "Advanced": 3}
+ topic_prereq_order = {
+ "JavaScript": 1,
+ "Python": 1,
+ "SQL": 2,
+ "React": 3,
+ "Node.js": 3,
+ "Java": 3,
+ "Machine Learning": 4,
+ "Full Stack Web Development": 5,
+ }
+
+ ordered_courses_tuples = list(selected_courses_map.values())
+ ordered_courses_tuples.sort(
+ key=lambda item: (
+ difficulty_order.get(item[0]["difficulty"], 2),
+ topic_prereq_order.get(item[0]["topic"], 3),
+ )
+ )
+
+ ordered_raw_courses = [item[0] for item in ordered_courses_tuples]
+
+ # Build RecommendedCourse models
+ rec_models = []
+ for c, score in ordered_courses_tuples:
+ # Generate custom why_relevant
+ why_rel = f"Directly aligns with your target of becoming a {profile.target_role}."
+ if c["topic"] == "JavaScript" and "react" in query_lower:
+ why_rel = "Bridges your basic JavaScript understanding to modern ES6+ patterns, closures, and async/await needed to write idiomatic React."
+ elif c["topic"] == "React":
+ why_rel = "Provides the core component, hooks, and routing architecture required for modern frontend engineering."
+ elif c["topic"] == "Python" and "machine learning" in query_lower:
+ why_rel = "Establishes data structure, algorithm, and scripting competence required before tackling ML algorithms."
+ elif c["topic"] == "Node.js":
+ why_rel = "Teaches server-side REST architecture and authentication to make your applications full-stack."
+ elif c["topic"] == "SQL":
+ why_rel = "Essential for designing normalized databases, writing analytical queries, and connecting data backends."
+ elif c["topic"] == "Machine Learning":
+ why_rel = "Guides you through hands-on Scikit-Learn pipelines, feature engineering, and model deployment."
+ elif c["topic"] == "Java":
+ why_rel = "Covers core object-oriented principles, JVM memory architecture, and Spring Boot 3 microservice persistence."
+
+ rec_models.append(
+ RecommendedCourse(
+ id=c["id"],
+ title=c["title"],
+ topic=c["topic"],
+ slug=c["slug"],
+ difficulty=c["difficulty"],
+ estimated_duration=c["estimated_duration"],
+ total_hours=c["total_hours"],
+ resource_types=c.get("resource_types", []),
+ relevance_score=round(score, 2),
+ why_relevant=why_rel,
+ topics_covered=c.get("topics_covered", []),
+ learning_sequence=c.get("learning_sequence", []),
+ key_skills_acquired=c.get("key_skills_acquired", []),
+ capstone_project=c.get("capstone_project", {}),
+ )
+ )
+
+ # Generate Learning Sequence Phases
+ learning_sequence = []
+ total_weeks = 0.0
+
+ for idx, c in enumerate(ordered_raw_courses):
+ phase_num = idx + 1
+ est_weeks = max(2.0, round(c["total_hours"] / max(4, profile.weekly_commitment_hours), 1))
+ total_weeks += est_weeks
+
+ phase_title = f"Phase {phase_num}: Master {c['title']}"
+ weekly_focus = f"Dedicate ~{profile.weekly_commitment_hours} hours/week exploring {c['topic']} concepts and coding exercises."
+ milestone_goal = f"Complete all modules and successfully deliver the capstone project: {c['capstone_project'].get('title', 'Course Project')}."
+ actionable_project = c["capstone_project"].get("title", f"{c['topic']} Applied Project")
+
+ learning_sequence.append(
+ LearningPhase(
+ phase_number=phase_num,
+ phase_title=phase_title,
+ course_id=c["id"],
+ course_title=c["title"],
+ duration_weeks=est_weeks,
+ weekly_focus=weekly_focus,
+ milestone_goal=milestone_goal,
+ actionable_project=actionable_project,
+ )
+ )
+
+ # Formulate customized concise summary (no giant monolithic paragraphs!)
+ summary = (
+ f"Customized milestone path to advance from **{profile.current_level}** to a proficient **{profile.target_role}** "
+ f"within **{profile.timeframe}**. Structured into progressive phases at ~**{profile.weekly_commitment_hours} hours/week** "
+ f"balancing foundational logic with hands-on capstone engineering."
+ )
+
+ why_recommended = (
+ f"This sequence systematically prevents knowledge gaps by building solid architecture before high-level frameworks. "
+ f"Every phase delivers an interactive milestone project to prove competency."
+ )
+
+ # Structured points & next step details
+ why_points = build_why_recommended_points(ordered_raw_courses, profile)
+ next_step_details = build_next_step_details(ordered_raw_courses[0], profile)
+
+ # Immediate next step string
+ first_course = ordered_raw_courses[0]
+ first_module = first_course.get("learning_sequence", [{}])[0]
+ immediate_next_step = (
+ f"Begin today with **{first_course['title']}** โ Start with **{first_module.get('title', 'Module 1')}**: "
+ f"{first_module.get('summary', 'Review core syntax and foundational logic')}. "
+ f"Hands-on goal for this week: *{first_module.get('hands_on_project', 'Complete initial practice exercise')}*."
+ )
+
+ # Build the full DAG dependency graph!
+ dag_graph = build_dag_graph(ordered_raw_courses, profile)
+
+ alternatives = [
+ "Increase study time to 12-15 hours/week to complete the roadmap in half the time.",
+ "Add an interactive Git & GitHub version control module to your workflow for portfolio sharing.",
+ "Team up with a peer in the LearnSphere Community channel for weekly code reviews."
+ ]
+
+ now_iso = datetime.now(timezone.utc).isoformat()
+
+ # Save to database
+ save_user_query(
+ query_id=query_id,
+ query_text=goal_req.query,
+ parsed_current_level=profile.current_level,
+ parsed_target_role=profile.target_role,
+ parsed_timeframe=profile.timeframe,
+ recommended_courses=[rc.model_dump() for rc in rec_models],
+ learning_sequence=[lp.model_dump() for lp in learning_sequence],
+ why_relevant=why_recommended,
+ immediate_next_step=immediate_next_step,
+ summary=summary,
+ user_id=goal_req.user_id,
+ )
+
+ return RecommendationResponse(
+ success=True,
+ query_id=query_id,
+ learner_profile=profile,
+ summary=summary,
+ why_recommended=why_recommended,
+ why_recommended_points=why_points,
+ immediate_next_step=immediate_next_step,
+ next_step_details=next_step_details,
+ learning_sequence=learning_sequence,
+ dag_graph=dag_graph,
+ recommended_courses=rec_models,
+ alternative_paths=alternatives,
+ created_at=now_iso,
+ )
+
+
+def generate_gemini_recommendation(
+ goal_req: GoalRequest,
+ courses: List[Dict[str, Any]],
+ scored_courses: List[Tuple[Dict[str, Any], float]],
+ profile: ParsedProfile,
+) -> Optional[RecommendationResponse]:
+ """
+ Uses Gemini LLM to synthesize rich, grounded natural language reasoning
+ based on the retrieved LearnSphere catalog.
+ """
+ if not gemini_client:
+ return None
+
+ # Retrieve candidate courses
+ candidate_courses = [item[0] for item in scored_courses[:5]]
+ catalog_summary = []
+ for c in candidate_courses:
+ catalog_summary.append({
+ "id": c["id"],
+ "title": c["title"],
+ "topic": c["topic"],
+ "difficulty": c["difficulty"],
+ "description": c["description"],
+ "prerequisites": c["prerequisites"],
+ "estimated_duration": c["estimated_duration"],
+ "total_hours": c["total_hours"],
+ "topics_covered": c["topics_covered"],
+ "capstone_project": c["capstone_project"],
+ })
+
+ prompt = f"""You are the official AI Learning Assistant for LearnSphere.
+A learner has described their learning goal and background:
+"{goal_req.query}"
+
+Learner Profile Details:
+- Detected Current Level: {profile.current_level}
+- Target Ambition: {profile.target_role}
+- Desired Timeframe: {profile.timeframe}
+- Weekly Commitment: {profile.weekly_commitment_hours} hours/week
+
+Below is the verified LearnSphere structured content catalog available:
+{json.dumps(catalog_summary, indent=2)}
+
+TASK:
+1. Select the best 1 to 4 courses from the LearnSphere catalog that bridge the learner's current level to their goal.
+2. Formulate a logical, sequential learning path with phases.
+3. Keep text concise and actionable (NO long monolithic paragraphs).
+4. Provide structured "why_recommended_points" with 3 to 4 distinct key takeaway cards (Target Alignment, Pacing Feasibility, Prerequisite Bridge, Portfolio Deliverable).
+5. Provide structured "next_step_details" with course_title, module_title, action_item, hands_on_project, estimated_time.
+
+You MUST respond strictly with valid JSON conforming to this exact structure:
+{{
+ "summary": "Concise 2-3 sentence overview of this milestone path",
+ "why_recommended": "Brief 2-sentence overall rationale summary",
+ "why_recommended_points": [
+ {{
+ "title": "Target Role Alignment",
+ "description": "Short explanation of how this path maps to their ambition",
+ "icon": "fa-bullseye"
+ }},
+ {{
+ "title": "Pacing & Feasibility",
+ "description": "Short explanation of weekly hours and timeline feasibility",
+ "icon": "fa-clock"
+ }},
+ {{
+ "title": "Prerequisite Bridge",
+ "description": "Short explanation of foundational skills solidified first",
+ "icon": "fa-bridge"
+ }},
+ {{
+ "title": "Portfolio Deliverable",
+ "description": "Short explanation of the capstone project built",
+ "icon": "fa-trophy"
+ }}
+ ],
+ "immediate_next_step": "Precise instruction of which course, module, and hands-on lab to start today",
+ "next_step_details": {{
+ "course_title": "Course Title",
+ "module_title": "Module 1: ...",
+ "action_item": "Specific action to do today",
+ "hands_on_project": "Hands-on coding exercise for this week",
+ "estimated_time": "Week 1 (6 hours)"
+ }},
+ "recommended_course_ids": ["course-java-springboot"],
+ "learning_sequence": [
+ {{
+ "phase_number": 1,
+ "phase_title": "Phase 1: ...",
+ "course_id": "course-java-springboot",
+ "course_title": "...",
+ "duration_weeks": 4.0,
+ "weekly_focus": "...",
+ "milestone_goal": "...",
+ "actionable_project": "..."
+ }}
+ ],
+ "alternative_paths": [
+ "Optional suggestion 1",
+ "Optional suggestion 2"
+ ]
+}}
+Only return the raw JSON object, no markdown code fence and no extra commentary.
+"""
+
+ try:
+ response = gemini_client.models.generate_content(
+ model=GEMINI_MODEL,
+ contents=prompt,
+ )
+ raw_text = (response.text or "").strip()
+ # Remove markdown fence if present
+ raw_text = re.sub(r"^```(json)?\s*", "", raw_text)
+ raw_text = re.sub(r"\s*```$", "", raw_text)
+
+ parsed = json.loads(raw_text)
+
+ query_id = f"ls-query-{uuid.uuid4().hex[:8]}"
+
+ rec_course_ids = parsed.get("recommended_course_ids", [])
+ rec_models = []
+ raw_courses = []
+ for cid in rec_course_ids:
+ c = get_course_by_id(cid)
+ if c:
+ raw_courses.append(c)
+ rec_models.append(
+ RecommendedCourse(
+ id=c["id"],
+ title=c["title"],
+ topic=c["topic"],
+ slug=c["slug"],
+ difficulty=c["difficulty"],
+ estimated_duration=c["estimated_duration"],
+ total_hours=c["total_hours"],
+ resource_types=c.get("resource_types", []),
+ relevance_score=0.95,
+ why_relevant=f"Selected by AI as a core milestone for {profile.target_role}.",
+ topics_covered=c.get("topics_covered", []),
+ learning_sequence=c.get("learning_sequence", []),
+ key_skills_acquired=c.get("key_skills_acquired", []),
+ capstone_project=c.get("capstone_project", {}),
+ )
+ )
+
+ if not rec_models:
+ # If Gemini returned invalid IDs, fallback to heuristic
+ return None
+
+ phases = []
+ for p in parsed.get("learning_sequence", []):
+ phases.append(LearningPhase(**p))
+
+ # Build DAG Graph from verified courses
+ dag_graph = build_dag_graph(raw_courses, profile)
+
+ # Parse why_recommended_points
+ why_points = []
+ raw_points = parsed.get("why_recommended_points", [])
+ if isinstance(raw_points, list) and len(raw_points) > 0:
+ for pt in raw_points:
+ try:
+ why_points.append(KeyTakeaway(
+ title=pt.get("title", "Key Takeaway"),
+ description=pt.get("description", ""),
+ icon=pt.get("icon", "fa-check")
+ ))
+ except Exception:
+ pass
+ if not why_points:
+ why_points = build_why_recommended_points(raw_courses, profile)
+
+ # Parse next_step_details
+ raw_next = parsed.get("next_step_details", {})
+ if isinstance(raw_next, dict) and "course_title" in raw_next:
+ try:
+ next_step_details = NextStepDetails(
+ course_title=raw_next.get("course_title", raw_courses[0]["title"]),
+ module_title=raw_next.get("module_title", "Module 1"),
+ action_item=raw_next.get("action_item", "Start initial foundational lab"),
+ hands_on_project=raw_next.get("hands_on_project", "Build starter exercise"),
+ estimated_time=raw_next.get("estimated_time", "Week 1 (6 hours)")
+ )
+ except Exception:
+ next_step_details = build_next_step_details(raw_courses[0], profile)
+ else:
+ next_step_details = build_next_step_details(raw_courses[0], profile)
+
+ now_iso = datetime.now(timezone.utc).isoformat()
+
+ # Save to database
+ save_user_query(
+ query_id=query_id,
+ query_text=goal_req.query,
+ parsed_current_level=profile.current_level,
+ parsed_target_role=profile.target_role,
+ parsed_timeframe=profile.timeframe,
+ recommended_courses=[rc.model_dump() for rc in rec_models],
+ learning_sequence=[lp.model_dump() for lp in phases],
+ why_relevant=parsed.get("why_recommended", ""),
+ immediate_next_step=parsed.get("immediate_next_step", ""),
+ summary=parsed.get("summary", ""),
+ user_id=goal_req.user_id,
+ )
+
+ return RecommendationResponse(
+ success=True,
+ query_id=query_id,
+ learner_profile=profile,
+ summary=parsed.get("summary", ""),
+ why_recommended=parsed.get("why_recommended", ""),
+ why_recommended_points=why_points,
+ immediate_next_step=parsed.get("immediate_next_step", ""),
+ next_step_details=next_step_details,
+ learning_sequence=phases,
+ dag_graph=dag_graph,
+ recommended_courses=rec_models,
+ alternative_paths=parsed.get("alternative_paths", []),
+ created_at=now_iso,
+ )
+ except Exception as e:
+ print(f"[AI ENGINE] Gemini generation error: {e}. Falling back to deterministic engine.")
+ return None
+
+
+def get_personalized_recommendation(goal_req: GoalRequest) -> RecommendationResponse:
+ """
+ Main entry point for generating personalized recommendations:
+ 1. Loads structured courses from SQLite.
+ 2. Computes semantic relevance vector scores.
+ 3. Parses learner profile.
+ 4. Tries Gemini LLM with grounded catalog; falls back to deterministic heuristic engine.
+ """
+ courses = get_all_courses()
+ scored_courses = compute_semantic_relevance(goal_req.query, courses)
+ profile = parse_learner_profile(goal_req.query, goal_req.current_level, goal_req.target_timeframe)
+
+ # Attempt Gemini generation if client is configured
+ if gemini_client:
+ llm_response = generate_gemini_recommendation(goal_req, courses, scored_courses, profile)
+ if llm_response:
+ return llm_response
+
+ # Fallback to rule-based & semantic progression engine
+ return generate_heuristic_recommendation(goal_req, courses, scored_courses, profile)
+
+
+def handle_followup_chat(chat_req: ChatRequest) -> ChatResponse:
+ """
+ Handles conversational follow-up questions from the learner regarding their plan.
+ """
+ session_id = chat_req.session_id or f"sess-{uuid.uuid4().hex[:8]}"
+ msg_id = f"msg-{uuid.uuid4().hex[:8]}"
+
+ # Save user message
+ save_chat_message(session_id, "user", chat_req.message, msg_id, chat_req.context_query_id)
+ history = get_chat_history(session_id, limit=8)
+
+ all_courses = get_all_courses()
+ catalog_brief = ", ".join([f"{c['title']} ({c['topic']}, {c['difficulty']})" for c in all_courses])
+
+ if gemini_client:
+ try:
+ history_prompt = "\n".join([f"{h['role'].upper()}: {h['message']}" for h in history])
+ prompt = (
+ f"You are the LearnSphere AI Learning Assistant.\n"
+ f"Available LearnSphere Courses: {catalog_brief}\n\n"
+ f"Conversation History:\n{history_prompt}\n\n"
+ f"Respond to the learner's latest question with helpful, encouraging, and concrete advice grounded in LearnSphere's courses.\n"
+ f"Be concise, practical, and clear."
+ )
+ response = gemini_client.models.generate_content(
+ model=GEMINI_MODEL,
+ contents=prompt,
+ )
+ reply = (response.text or "").strip()
+ except Exception as e:
+ print(f"[AI ENGINE] Chat LLM error: {e}")
+ reply = generate_heuristic_chat_reply(chat_req.message)
+ else:
+ reply = generate_heuristic_chat_reply(chat_req.message)
+
+ # Save assistant message
+ asst_msg_id = f"msg-{uuid.uuid4().hex[:8]}"
+ save_chat_message(session_id, "assistant", reply, asst_msg_id, chat_req.context_query_id)
+
+ suggested = [
+ "How many hours per week should I dedicate to labs?",
+ "What projects will best showcase my skills to employers?",
+ "Can I skip straight to the intermediate course?"
+ ]
+
+ return ChatResponse(session_id=session_id, reply=reply, suggested_prompts=suggested)
+
+
+def generate_heuristic_chat_reply(message: str) -> str:
+ """
+ Rule-based intelligent chat response when LLM is unavailable.
+ """
+ m = message.lower()
+ if any(k in m for k in ["hours", "time", "pace", "schedule", "busy", "part-time"]):
+ return (
+ "If your schedule is tight, we recommend breaking your study time into 45-minute daily focus sessions "
+ "rather than long weekend marathons. Dedicating 5โ7 hours across 4โ5 days yields significantly higher retention "
+ "and allows your subconscious mind to absorb programming patterns between sessions."
+ )
+ elif any(k in m for k in ["skip", "prerequisite", "direct", "fast"]):
+ return (
+ "While you can jump directly into intermediate topics like React or Spring Boot, learners who brush up on core foundations "
+ "(such as ES6+ closures, async/await, or Java Streams) solve debugging issues 3x faster. We recommend reviewing the first module's "
+ "interactive quiz firstโif you score 85%+, feel free to skip ahead!"
+ )
+ elif any(k in m for k in ["project", "portfolio", "resume", "job", "hire"]):
+ return (
+ "The capstone projects in each course are specifically designed for your portfolio! For example, the E-Commerce Storefront in React "
+ "and the Loan Prediction API in Machine Learning demonstrate real-world architecture, error handling, and deployability that recruiters look for."
+ )
+ elif any(k in m for k in ["stuck", "error", "difficult", "hard", "help"]):
+ return (
+ "Getting stuck is a natural part of coding! Try breaking down the problem: log each intermediate variable, "
+ "check MDN or official documentation, and re-read the module cheatsheet. You can also paste specific error messages here for guidance."
+ )
+ else:
+ return (
+ "Great question! LearnSphere's structured content provides step-by-step guidance, interactive labs, and practical capstone projects. "
+ "Focus on completing each module's hands-on exercise before moving to the next. What specific topic or milestone would you like to explore next?"
+ )
diff --git a/backend/database.py b/backend/database.py
new file mode 100644
index 0000000..6d05749
--- /dev/null
+++ b/backend/database.py
@@ -0,0 +1,324 @@
+import json
+import os
+import sqlite3
+import datetime
+from pathlib import Path
+from typing import List, Dict, Any, Optional
+
+DB_DIR = Path(__file__).resolve().parent / "data"
+DB_DIR.mkdir(parents=True, exist_ok=True)
+DB_PATH = DB_DIR / "learnsphere_assistant.db"
+SEED_FILE = Path(__file__).resolve().parent / "seed_data.json"
+
+
+def get_db_connection() -> sqlite3.Connection:
+ conn = sqlite3.connect(str(DB_PATH))
+ conn.row_factory = sqlite3.Row
+ return conn
+
+
+def init_db():
+ conn = get_db_connection()
+ cursor = conn.cursor()
+
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS learning_content (
+ id TEXT PRIMARY KEY,
+ title TEXT NOT NULL,
+ topic TEXT NOT NULL,
+ slug TEXT NOT NULL UNIQUE,
+ difficulty TEXT NOT NULL,
+ description TEXT NOT NULL,
+ prerequisites TEXT NOT NULL,
+ estimated_duration TEXT NOT NULL,
+ total_hours INTEGER NOT NULL,
+ resource_types TEXT NOT NULL,
+ target_roles TEXT NOT NULL,
+ topics_covered TEXT NOT NULL,
+ learning_sequence TEXT NOT NULL,
+ key_skills_acquired TEXT NOT NULL,
+ capstone_project TEXT NOT NULL,
+ created_at TEXT NOT NULL
+ );
+ """)
+
+ cursor.execute("""
+ CREATE INDEX IF NOT EXISTS ix_learning_content_topic ON learning_content(topic);
+ """)
+ cursor.execute("""
+ CREATE INDEX IF NOT EXISTS ix_learning_content_difficulty ON learning_content(difficulty);
+ """)
+
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS user_queries (
+ id TEXT PRIMARY KEY,
+ user_id TEXT,
+ query_text TEXT NOT NULL,
+ parsed_current_level TEXT,
+ parsed_target_role TEXT,
+ parsed_timeframe TEXT,
+ recommended_courses TEXT NOT NULL,
+ learning_sequence TEXT NOT NULL,
+ why_relevant TEXT NOT NULL,
+ immediate_next_step TEXT NOT NULL,
+ summary TEXT,
+ created_at TEXT NOT NULL
+ );
+ """)
+ cursor.execute("""
+ CREATE INDEX IF NOT EXISTS ix_user_queries_created_at ON user_queries(created_at DESC);
+ """)
+
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS chat_interactions (
+ id TEXT PRIMARY KEY,
+ session_id TEXT NOT NULL,
+ role TEXT NOT NULL,
+ message TEXT NOT NULL,
+ context_query_id TEXT,
+ created_at TEXT NOT NULL
+ );
+ """)
+ cursor.execute("""
+ CREATE INDEX IF NOT EXISTS ix_chat_session ON chat_interactions(session_id);
+ """)
+
+ conn.commit()
+ conn.close()
+
+ # Seed content if table is empty or update if updated
+ seed_database_if_needed()
+
+
+def seed_database_if_needed(force: bool = False):
+ if not SEED_FILE.exists():
+ return
+
+ conn = get_db_connection()
+ cursor = conn.cursor()
+
+ cursor.execute("SELECT COUNT(*) FROM learning_content")
+ count = cursor.fetchone()[0]
+
+ if count > 0 and not force:
+ conn.close()
+ return
+
+ with open(SEED_FILE, "r", encoding="utf-8") as f:
+ courses = json.load(f)
+
+ now = datetime.datetime.now(datetime.timezone.utc).isoformat()
+
+ for c in courses:
+ cursor.execute("""
+ INSERT INTO learning_content (
+ id, title, topic, slug, difficulty, description, prerequisites,
+ estimated_duration, total_hours, resource_types, target_roles,
+ topics_covered, learning_sequence, key_skills_acquired,
+ capstone_project, created_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ON CONFLICT(id) DO UPDATE SET
+ title=excluded.title,
+ topic=excluded.topic,
+ slug=excluded.slug,
+ difficulty=excluded.difficulty,
+ description=excluded.description,
+ prerequisites=excluded.prerequisites,
+ estimated_duration=excluded.estimated_duration,
+ total_hours=excluded.total_hours,
+ resource_types=excluded.resource_types,
+ target_roles=excluded.target_roles,
+ topics_covered=excluded.topics_covered,
+ learning_sequence=excluded.learning_sequence,
+ key_skills_acquired=excluded.key_skills_acquired,
+ capstone_project=excluded.capstone_project
+ """, (
+ c["id"],
+ c["title"],
+ c["topic"],
+ c["slug"],
+ c["difficulty"],
+ c["description"],
+ json.dumps(c.get("prerequisites", [])),
+ c["estimated_duration"],
+ c.get("total_hours", 0),
+ json.dumps(c.get("resource_types", [])),
+ json.dumps(c.get("target_roles", [])),
+ json.dumps(c.get("topics_covered", [])),
+ json.dumps(c.get("learning_sequence", [])),
+ json.dumps(c.get("key_skills_acquired", [])),
+ json.dumps(c.get("capstone_project", {})),
+ now
+ ))
+
+ conn.commit()
+ conn.close()
+
+
+def get_all_courses(topic: Optional[str] = None, difficulty: Optional[str] = None) -> List[Dict[str, Any]]:
+ conn = get_db_connection()
+ cursor = conn.cursor()
+
+ query = "SELECT * FROM learning_content WHERE 1=1"
+ params = []
+
+ if topic and topic.lower() != "all":
+ query += " AND LOWER(topic) = LOWER(?)"
+ params.append(topic)
+ if difficulty and difficulty.lower() != "all":
+ query += " AND LOWER(difficulty) = LOWER(?)"
+ params.append(difficulty)
+
+ query += " ORDER BY total_hours ASC"
+ cursor.execute(query, params)
+ rows = cursor.fetchall()
+ conn.close()
+
+ results = []
+ for r in rows:
+ results.append({
+ "id": r["id"],
+ "title": r["title"],
+ "topic": r["topic"],
+ "slug": r["slug"],
+ "difficulty": r["difficulty"],
+ "description": r["description"],
+ "prerequisites": json.loads(r["prerequisites"]),
+ "estimated_duration": r["estimated_duration"],
+ "total_hours": r["total_hours"],
+ "resource_types": json.loads(r["resource_types"]),
+ "target_roles": json.loads(r["target_roles"]),
+ "topics_covered": json.loads(r["topics_covered"]),
+ "learning_sequence": json.loads(r["learning_sequence"]),
+ "key_skills_acquired": json.loads(r["key_skills_acquired"]),
+ "capstone_project": json.loads(r["capstone_project"]),
+ "created_at": r["created_at"],
+ })
+ return results
+
+
+def get_course_by_id(course_id: str) -> Optional[Dict[str, Any]]:
+ conn = get_db_connection()
+ cursor = conn.cursor()
+ cursor.execute("SELECT * FROM learning_content WHERE id = ? OR slug = ?", (course_id, course_id))
+ r = cursor.fetchone()
+ conn.close()
+
+ if not r:
+ return None
+
+ return {
+ "id": r["id"],
+ "title": r["title"],
+ "topic": r["topic"],
+ "slug": r["slug"],
+ "difficulty": r["difficulty"],
+ "description": r["description"],
+ "prerequisites": json.loads(r["prerequisites"]),
+ "estimated_duration": r["estimated_duration"],
+ "total_hours": r["total_hours"],
+ "resource_types": json.loads(r["resource_types"]),
+ "target_roles": json.loads(r["target_roles"]),
+ "topics_covered": json.loads(r["topics_covered"]),
+ "learning_sequence": json.loads(r["learning_sequence"]),
+ "key_skills_acquired": json.loads(r["key_skills_acquired"]),
+ "capstone_project": json.loads(r["capstone_project"]),
+ "created_at": r["created_at"],
+ }
+
+
+def save_user_query(
+ query_id: str,
+ query_text: str,
+ parsed_current_level: str,
+ parsed_target_role: str,
+ parsed_timeframe: str,
+ recommended_courses: List[Any],
+ learning_sequence: List[Any],
+ why_relevant: str,
+ immediate_next_step: str,
+ summary: str = "",
+ user_id: Optional[str] = None
+):
+ conn = get_db_connection()
+ cursor = conn.cursor()
+ now = datetime.datetime.now(datetime.timezone.utc).isoformat()
+
+ cursor.execute("""
+ INSERT INTO user_queries (
+ id, user_id, query_text, parsed_current_level, parsed_target_role,
+ parsed_timeframe, recommended_courses, learning_sequence, why_relevant,
+ immediate_next_step, summary, created_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ """, (
+ query_id,
+ user_id,
+ query_text,
+ parsed_current_level,
+ parsed_target_role,
+ parsed_timeframe,
+ json.dumps(recommended_courses),
+ json.dumps(learning_sequence),
+ why_relevant,
+ immediate_next_step,
+ summary,
+ now
+ ))
+ conn.commit()
+ conn.close()
+
+
+def get_recent_queries(limit: int = 10) -> List[Dict[str, Any]]:
+ conn = get_db_connection()
+ cursor = conn.cursor()
+ cursor.execute("""
+ SELECT * FROM user_queries ORDER BY created_at DESC LIMIT ?
+ """, (limit,))
+ rows = cursor.fetchall()
+ conn.close()
+
+ results = []
+ for r in rows:
+ results.append({
+ "id": r["id"],
+ "user_id": r["user_id"],
+ "query_text": r["query_text"],
+ "parsed_current_level": r["parsed_current_level"],
+ "parsed_target_role": r["parsed_target_role"],
+ "parsed_timeframe": r["parsed_timeframe"],
+ "recommended_courses": json.loads(r["recommended_courses"]),
+ "learning_sequence": json.loads(r["learning_sequence"]),
+ "why_relevant": r["why_relevant"],
+ "immediate_next_step": r["immediate_next_step"],
+ "summary": r["summary"],
+ "created_at": r["created_at"],
+ })
+ return results
+
+
+def save_chat_message(session_id: str, role: str, message: str, message_id: str, context_query_id: Optional[str] = None):
+ conn = get_db_connection()
+ cursor = conn.cursor()
+ now = datetime.datetime.now(datetime.timezone.utc).isoformat()
+
+ cursor.execute("""
+ INSERT INTO chat_interactions (id, session_id, role, message, context_query_id, created_at)
+ VALUES (?, ?, ?, ?, ?, ?)
+ """, (message_id, session_id, role, message, context_query_id, now))
+ conn.commit()
+ conn.close()
+
+
+def get_chat_history(session_id: str, limit: int = 20) -> List[Dict[str, Any]]:
+ conn = get_db_connection()
+ cursor = conn.cursor()
+ cursor.execute("""
+ SELECT role, message, created_at FROM chat_interactions
+ WHERE session_id = ?
+ ORDER BY created_at ASC
+ LIMIT ?
+ """, (session_id, limit))
+ rows = cursor.fetchall()
+ conn.close()
+
+ return [{"role": r["role"], "message": r["message"], "created_at": r["created_at"]} for r in rows]
diff --git a/backend/main.py b/backend/main.py
new file mode 100644
index 0000000..726bfc0
--- /dev/null
+++ b/backend/main.py
@@ -0,0 +1,126 @@
+import html
+import os
+import uvicorn
+from contextlib import asynccontextmanager
+from typing import Optional, Dict, Any
+
+from fastapi import FastAPI, Request, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import JSONResponse
+from pydantic import BaseModel
+
+from backend.database import init_db
+from backend.routes import router as ai_router
+from backend.ai_engine import gemini_client, GEMINI_MODEL
+
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ # Initialize SQLite database and seed structured content
+ print("[SERVER] Initializing LearnSphere AI Learning Assistant database...")
+ init_db()
+ print("[SERVER] Database initialized and content verified.")
+ yield
+
+
+app = FastAPI(
+ title="LearnSphere AI Learning Assistant API",
+ description="Backend API powering personalized curriculum recommendations, semantic content retrieval, and AI tutoring for LearnSphere.",
+ version="2.0.0",
+ lifespan=lifespan,
+)
+
+# Enable CORS for frontend Vite dev server (port 5173), production URLs, and local requests
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+# Mount AI Learning Assistant routes
+app.include_router(ai_router)
+
+
+# Health check
+@app.get("/health")
+def health():
+ return {
+ "status": "ok",
+ "service": "learnsphere-ai-assistant",
+ "version": "2.0.0",
+ "ai_provider": "google-gemini" if gemini_client else "deterministic-semantic-engine",
+ }
+
+
+# Legacy /chat compatibility for home.html & review.html AI Tutor
+class LegacyChatRequest(BaseModel):
+ message: str
+
+
+@app.post("/chat")
+def legacy_chat(payload: LegacyChatRequest):
+ user_input = payload.message.strip()
+ if not user_input:
+ raise HTTPException(status_code=400, detail="Message cannot be empty.")
+
+ if gemini_client:
+ try:
+ guardrails = (
+ "You are an encouraging educational tutor for LearnSphere. "
+ "Help the student with concepts in programming, computer science, mathematics, or science. "
+ "Keep responses concise, clear, and actionable."
+ )
+ response = gemini_client.models.generate_content(
+ model=GEMINI_MODEL,
+ contents=f"{guardrails}\n\nUser: {user_input}",
+ )
+ raw = (response.text or "").strip()
+ safe_text = html.escape(raw).replace("\n", " ")
+ return {"reply": safe_text}
+ except Exception as e:
+ print(f"[LEGACY CHAT ERROR] {e}")
+
+ # Fallback response
+ return {
+ "reply": f"Hello! I am your LearnSphere Assistant. You asked about: '{html.escape(user_input)}'. Explore our structured courses or visit the AI Assistant tab for a personalized learning roadmap!"
+ }
+
+
+# Legacy /explain_mistake compatibility
+class ExplainMistakeRequest(BaseModel):
+ question: str
+ learnerAnswer: str
+ correctAnswer: str
+ topic: Optional[str] = "general"
+
+
+@app.post("/explain_mistake")
+def legacy_explain_mistake(payload: ExplainMistakeRequest):
+ if gemini_client:
+ try:
+ prompt = (
+ f"You are a helpful educational tutor. A learner answered a question incorrectly.\n"
+ f"Topic: {payload.topic}\n"
+ f"Question: {payload.question}\n"
+ f"Learner's answer: {payload.learnerAnswer}\n"
+ f"Correct answer: {payload.correctAnswer}\n\n"
+ f"Explain concisely: 1) Why the learner's answer is mistaken. 2) The core principle behind the correct answer."
+ )
+ response = gemini_client.models.generate_content(
+ model=GEMINI_MODEL,
+ contents=prompt,
+ )
+ raw = (response.text or "").strip()
+ return {"reply": html.escape(raw).replace("\n", " ")}
+ except Exception as e:
+ print(f"[EXPLAIN MISTAKE ERROR] {e}")
+
+ return {
+ "reply": f"The correct answer is '{html.escape(payload.correctAnswer)}' because it directly satisfies the problem constraints, whereas '{html.escape(payload.learnerAnswer)}' misses key foundational conditions."
+ }
+
+
+if __name__ == "__main__":
+ uvicorn.run("backend.main:app", host="0.0.0.0", port=5000, reload=True)
diff --git a/backend/models.py b/backend/models.py
new file mode 100644
index 0000000..d229d32
--- /dev/null
+++ b/backend/models.py
@@ -0,0 +1,117 @@
+from typing import List, Dict, Any, Optional
+from pydantic import BaseModel, Field
+
+
+class GoalRequest(BaseModel):
+ query: str = Field(..., min_length=3, max_length=1500, description="Natural language description of learning goal and background")
+ current_level: Optional[str] = Field("auto", description="Learner level: beginner, intermediate, advanced, or auto")
+ target_timeframe: Optional[str] = Field(None, description="Desired completion timeframe, e.g. '3 months', '6 weeks'")
+ target_role: Optional[str] = Field(None, description="Target career role, e.g. 'React Developer', 'Full Stack Engineer'")
+ weekly_hours: Optional[int] = Field(None, ge=1, le=80, description="Available study hours per week")
+ user_id: Optional[str] = Field(None, description="Optional user ID for tracking")
+
+
+class ParsedProfile(BaseModel):
+ current_level: str
+ target_role: str
+ timeframe: str
+ weekly_commitment_hours: int
+ primary_skills_needed: List[str]
+ prerequisites_to_bridge: List[str]
+
+
+class RecommendedCourse(BaseModel):
+ id: str
+ title: str
+ topic: str
+ slug: str
+ difficulty: str
+ estimated_duration: str
+ total_hours: int
+ resource_types: List[str]
+ relevance_score: float
+ why_relevant: str
+ topics_covered: List[str]
+ learning_sequence: List[Dict[str, Any]]
+ key_skills_acquired: List[str]
+ capstone_project: Dict[str, Any]
+
+
+class LearningPhase(BaseModel):
+ phase_number: int
+ phase_title: str
+ course_id: str
+ course_title: str
+ duration_weeks: float
+ weekly_focus: str
+ milestone_goal: str
+ actionable_project: str
+
+
+class KeyTakeaway(BaseModel):
+ title: str
+ description: str
+ icon: str = "fa-check"
+
+
+class NextStepDetails(BaseModel):
+ course_title: str
+ module_title: str
+ action_item: str
+ hands_on_project: str
+ estimated_time: str
+
+
+class DAGNode(BaseModel):
+ id: str
+ title: str
+ level: int # Topological rank / depth level in DAG (0, 1, 2, 3...)
+ phase: int
+ category: str # Foundation, Core, Advanced, Framework, Capstone
+ estimated_hours: int
+ duration_label: str
+ course_id: str
+ course_title: str
+ prerequisites: List[str] = []
+ key_topics: List[str] = []
+ hands_on_project: Optional[str] = None
+ status: str = "pending" # unlocked, in_progress, pending
+
+
+class DAGEdge(BaseModel):
+ from_node: str
+ to_node: str
+ label: str = "unlocks"
+
+
+class DAGGraph(BaseModel):
+ nodes: List[DAGNode] = []
+ edges: List[DAGEdge] = []
+
+
+class RecommendationResponse(BaseModel):
+ success: bool = True
+ query_id: str
+ learner_profile: ParsedProfile
+ summary: str
+ why_recommended: str
+ why_recommended_points: List[KeyTakeaway] = []
+ immediate_next_step: str
+ next_step_details: Optional[NextStepDetails] = None
+ learning_sequence: List[LearningPhase]
+ dag_graph: DAGGraph = DAGGraph()
+ recommended_courses: List[RecommendedCourse]
+ alternative_paths: List[str] = []
+ created_at: str
+
+
+class ChatRequest(BaseModel):
+ session_id: Optional[str] = None
+ message: str = Field(..., min_length=1, max_length=2000)
+ context_query_id: Optional[str] = None
+
+
+class ChatResponse(BaseModel):
+ session_id: str
+ reply: str
+ suggested_prompts: List[str] = []
diff --git a/backend/routes.py b/backend/routes.py
new file mode 100644
index 0000000..eddd184
--- /dev/null
+++ b/backend/routes.py
@@ -0,0 +1,95 @@
+from typing import Optional, List, Dict, Any
+from fastapi import APIRouter, HTTPException, Query
+
+from backend.database import (
+ get_all_courses,
+ get_course_by_id,
+ get_recent_queries,
+)
+from backend.models import (
+ GoalRequest,
+ RecommendationResponse,
+ ChatRequest,
+ ChatResponse,
+)
+from backend.ai_engine import (
+ get_personalized_recommendation,
+ handle_followup_chat,
+)
+
+router = APIRouter(prefix="/api/ai", tags=["AI Learning Assistant"])
+
+
+@router.get("/content", summary="List all structured learning courses")
+def list_courses(
+ topic: Optional[str] = Query(None, description="Filter by topic (e.g. JavaScript, React, Python, etc.)"),
+ difficulty: Optional[str] = Query(None, description="Filter by difficulty (Beginner, Intermediate, Advanced)"),
+):
+ try:
+ courses = get_all_courses(topic=topic, difficulty=difficulty)
+ return {
+ "success": True,
+ "total": len(courses),
+ "courses": courses,
+ }
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Failed to fetch content: {str(e)}")
+
+
+@router.get("/content/{course_id}", summary="Get detailed information for a single course")
+def get_course_detail(course_id: str):
+ course = get_course_by_id(course_id)
+ if not course:
+ raise HTTPException(status_code=404, detail=f"Course '{course_id}' not found.")
+ return {"success": True, "course": course}
+
+
+@router.post("/recommend", response_model=RecommendationResponse, summary="Get personalized learning recommendations")
+def recommend_path(req: GoalRequest):
+ if not req.query or len(req.query.strip()) < 3:
+ raise HTTPException(status_code=400, detail="Query must be at least 3 characters long.")
+ try:
+ return get_personalized_recommendation(req)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Recommendation error: {str(e)}")
+
+
+@router.post("/chat", response_model=ChatResponse, summary="Conversational follow-up assistant")
+def chat_with_assistant(req: ChatRequest):
+ if not req.message or len(req.message.strip()) == 0:
+ raise HTTPException(status_code=400, detail="Message cannot be empty.")
+ try:
+ return handle_followup_chat(req)
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Chat error: {str(e)}")
+
+
+@router.get("/history", summary="Get recent recommendation history")
+def query_history(limit: int = Query(10, ge=1, le=50)):
+ try:
+ queries = get_recent_queries(limit=limit)
+ return {"success": True, "history": queries}
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"History error: {str(e)}")
+
+
+@router.get("/stats", summary="Get content catalog and usage statistics")
+def catalog_stats():
+ try:
+ all_courses = get_all_courses()
+ topics = list({c["topic"] for c in all_courses})
+ difficulties = list({c["difficulty"] for c in all_courses})
+ total_hours = sum(c.get("total_hours", 0) for c in all_courses)
+ total_modules = sum(len(c.get("learning_sequence", [])) for c in all_courses)
+
+ return {
+ "success": True,
+ "total_courses": len(all_courses),
+ "topics_count": len(topics),
+ "topics": sorted(topics),
+ "difficulties": sorted(difficulties),
+ "total_curriculum_hours": total_hours,
+ "total_learning_modules": total_modules,
+ }
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=f"Stats error: {str(e)}")
diff --git a/backend/seed_data.json b/backend/seed_data.json
new file mode 100644
index 0000000..82eb413
--- /dev/null
+++ b/backend/seed_data.json
@@ -0,0 +1,580 @@
+[
+ {
+ "id": "course-js-core",
+ "title": "Modern JavaScript: Core to Advanced ES6+",
+ "topic": "JavaScript",
+ "slug": "modern-javascript-core",
+ "difficulty": "Beginner",
+ "description": "Master the world's most popular programming language from basic syntax to advanced asynchronous patterns, closures, event loops, and modern ES6+ features necessary for professional web development.",
+ "prerequisites": ["Basic computer literacy", "Familiarity with HTML & CSS syntax"],
+ "estimated_duration": "4 weeks (5-6 hours/week)",
+ "total_hours": 24,
+ "resource_types": ["Interactive Lab", "Video Lectures", "Project Guided", "Documentation & Cheatsheets", "Assessment Quiz"],
+ "target_roles": ["Frontend Developer", "Full Stack Developer", "Web Developer"],
+ "topics_covered": [
+ "Variables, Data Types, and Type Coercion",
+ "Functions, Scope, Closures, and Execution Context",
+ "Arrays, Objects, and Functional Methods (map, filter, reduce)",
+ "DOM Manipulation and Event Handling",
+ "Asynchronous JS: Callbacks, Promises, async/await",
+ "Fetch API, RESTful Endpoints, and JSON Parsing",
+ "Event Loop, Microtasks, and Browser Runtime",
+ "ES Modules, Tooling (Vite/Webpack), and Clean Code"
+ ],
+ "learning_sequence": [
+ {
+ "step_number": 1,
+ "title": "Module 1: JavaScript Foundations & Execution Model",
+ "duration": "1 week (6 hours)",
+ "summary": "Understand how JavaScript executes code in browsers and Node.js.",
+ "topics": ["Primitive types vs Reference types", "Scope chain & Hoisting", "Functions & Arrow syntax"],
+ "hands_on_project": "Build an Interactive Unit Converter with Dynamic DOM Updates"
+ },
+ {
+ "step_number": 2,
+ "title": "Module 2: Data Structures & Functional Programming",
+ "duration": "1 week (6 hours)",
+ "summary": "Manipulate data collections idiomatically using array transformations.",
+ "topics": ["Array methods: map, filter, reduce, find", "Object destructuring & spread operator", "Pure functions & immutability"],
+ "hands_on_project": "Build an In-Memory Product Catalog with Multi-Filter Search"
+ },
+ {
+ "step_number": 3,
+ "title": "Module 3: Asynchronous JavaScript & Network Requests",
+ "duration": "1 week (6 hours)",
+ "summary": "Tackle asynchronous flows, concurrency, and external API communication.",
+ "topics": ["Promises & Promise.all", "async/await syntax & error handling", "Fetch API & HTTP status codes"],
+ "hands_on_project": "Build a Live Weather & Forecast Dashboard consuming OpenWeather API"
+ },
+ {
+ "step_number": 4,
+ "title": "Module 4: Browser Runtime, Event Loop & Capstone",
+ "duration": "1 week (6 hours)",
+ "summary": "Deep dive into the Event Loop, performance optimization, and modular code architecture.",
+ "topics": ["Macro-tasks vs Micro-tasks", "Debounce & Throttle techniques", "ES Modules and Local Storage"],
+ "hands_on_project": "Capstone: Kanban Task Management Board with Persistence & Drag-and-Drop"
+ }
+ ],
+ "key_skills_acquired": [
+ "Modern ES6+ syntax and best practices",
+ "Asynchronous programming with async/await and promises",
+ "DOM manipulation and responsive event handling",
+ "API communication and data transformation",
+ "Debugging runtime errors and performance bottlenecks"
+ ],
+ "capstone_project": {
+ "title": "Responsive Kanban Task Manager",
+ "description": "A full-featured Kanban board featuring column sorting, state persistence in localStorage, async external sync, and keyboard accessibility.",
+ "skills_demonstrated": ["ES6 Modules", "DOM Manipulation", "Asynchronous Events", "Local Storage"]
+ }
+ },
+ {
+ "id": "course-react-engineering",
+ "title": "React: Modern Frontend Engineering & State Architecture",
+ "topic": "React",
+ "slug": "react-frontend-engineering",
+ "difficulty": "Intermediate",
+ "description": "Learn to design, architect, and deploy declarative, scalable web interfaces using React 18+, modern hooks, state management, client-side routing, and real-world API integration.",
+ "prerequisites": ["Solid JavaScript ES6+ (destructuring, arrow functions, promises)", "DOM basics"],
+ "estimated_duration": "6 weeks (6-8 hours/week)",
+ "total_hours": 40,
+ "resource_types": ["Interactive Lab", "Video Lectures", "Project Guided", "Documentation & Cheatsheets", "Assessment Quiz"],
+ "target_roles": ["React Developer", "Frontend Engineer", "Full Stack Developer", "UI Engineer"],
+ "topics_covered": [
+ "JSX, Virtual DOM, and Component Composition",
+ "State & Props: Unidirectional Data Flow",
+ "Essential Hooks: useState, useEffect, useRef",
+ "Advanced Hooks: useMemo, useCallback, useReducer",
+ "Custom Hooks & Reusable Logic Extraction",
+ "Context API and Global State Management",
+ "Client-Side Routing with React Router v6",
+ "Forms, Validation, and Performance Optimization",
+ "Connecting to REST APIs and handling loading/error states"
+ ],
+ "learning_sequence": [
+ {
+ "step_number": 1,
+ "title": "Module 1: Mental Model of React & Declarative UI",
+ "duration": "1 week (6 hours)",
+ "summary": "Transition from imperative DOM to component-driven thinking.",
+ "topics": ["JSX syntax and compilation", "Functional components & Props", "Rendering lists & Conditional elements"],
+ "hands_on_project": "Build an Interactive Course Card Showcase with Dynamic Props"
+ },
+ {
+ "step_number": 2,
+ "title": "Module 2: State, Lifecycle & Side Effects with Hooks",
+ "duration": "1.5 weeks (10 hours)",
+ "summary": "Master state persistence and side effect management.",
+ "topics": ["useState for interactive inputs", "useEffect lifecycle and cleanup", "Synchronizing with browser APIs and timers"],
+ "hands_on_project": "Build a Pomodoro Productivity Dashboard with Audio Alerts & History"
+ },
+ {
+ "step_number": 3,
+ "title": "Module 3: Advanced Hooks & Custom Hook Architecture",
+ "duration": "1 week (8 hours)",
+ "summary": "Write maintainable, dry code by abstracting logic into reusable custom hooks.",
+ "topics": ["useReducer for complex state", "useMemo & useCallback for memoization", "Building useFetch and useDebounce custom hooks"],
+ "hands_on_project": "Build a Live Searchable Film & Media Explorer with Debounced Querying"
+ },
+ {
+ "step_number": 4,
+ "title": "Module 4: Routing, Navigation & Global State",
+ "duration": "1 week (8 hours)",
+ "summary": "Create multi-page web applications with shared state.",
+ "topics": ["React Router v6 nested routes and params", "Context API + useReducer pattern", "Protected routes & authentication flows"],
+ "hands_on_project": "Build a Multi-Page Learning Portal with Protected Student Dashboard"
+ },
+ {
+ "step_number": 5,
+ "title": "Module 5: Performance Optimization & Capstone Production App",
+ "duration": "1.5 weeks (8 hours)",
+ "summary": "Optimize render cycles, code splitting, lazy loading, and deploy.",
+ "topics": ["React.memo & Profiler", "Lazy loading with Suspense", "Build tooling with Vite & Vercel deployment"],
+ "hands_on_project": "Capstone: Full-featured E-Commerce Dashboard with Shopping Cart & Checkout"
+ }
+ ],
+ "key_skills_acquired": [
+ "Building modular, reusable React components",
+ "Proficient use of built-in and custom React Hooks",
+ "Architecting state with Context and useReducer",
+ "Seamless REST API integration with optimistic UI updates",
+ "Performance tuning and bundle optimization"
+ ],
+ "capstone_project": {
+ "title": "E-Commerce Storefront & Analytics Dashboard",
+ "description": "A production-grade React single page application with product filtering, cart drawer, checkout modal, theme switcher, and persistence.",
+ "skills_demonstrated": ["React 18", "Custom Hooks", "Context API", "React Router", "Responsive CSS"]
+ }
+ },
+ {
+ "id": "course-nodejs-backend",
+ "title": "Node.js & Express: Scalable Backend Architecture",
+ "topic": "Node.js",
+ "slug": "nodejs-express-backend",
+ "difficulty": "Intermediate",
+ "description": "Design and build fast, reliable, and secure RESTful backend services and APIs using Node.js, Express, asynchronous I/O, middleware architecture, and JWT authentication.",
+ "prerequisites": ["Intermediate JavaScript (Async/await, ES modules, error handling)"],
+ "estimated_duration": "5 weeks (6-7 hours/week)",
+ "total_hours": 32,
+ "resource_types": ["Interactive Lab", "Video Lectures", "Project Guided", "Documentation & Cheatsheets", "Assessment Quiz"],
+ "target_roles": ["Backend Developer", "Node.js Engineer", "Full Stack Developer", "API Engineer"],
+ "topics_covered": [
+ "Node.js Architecture: V8 engine, Libuv, and Event Loop",
+ "File System (fs), Streams, Buffers, and Path modules",
+ "Express.js Framework: Router, Controllers, and Service layers",
+ "Middleware Architecture (Logging, Auth, Validation, CORS)",
+ "RESTful API Design Standards & HTTP Status Codes",
+ "Authentication with JSON Web Tokens (JWT) and Password Hashing (bcrypt)",
+ "Database Integration with SQLite / PostgreSQL",
+ "Rate Limiting, Security Headers (Helmet), and Error Handling Middleware"
+ ],
+ "learning_sequence": [
+ {
+ "step_number": 1,
+ "title": "Module 1: Node Runtime & Async Core",
+ "duration": "1 week (6 hours)",
+ "summary": "Understand asynchronous I/O and core Node utilities.",
+ "topics": ["Node REPL and process object", "Event Emitter & Streams", "File system operations (fs/promises)"],
+ "hands_on_project": "Build an Automated Log File Analyzer and Report Generator CLI"
+ },
+ {
+ "step_number": 2,
+ "title": "Module 2: Building Clean REST APIs with Express",
+ "duration": "1.5 weeks (9 hours)",
+ "summary": "Master HTTP routing, parameters, body parsing, and status codes.",
+ "topics": ["Express application setup", "Route parameters, queries, and body parsing", "Controller-Service-Repository pattern"],
+ "hands_on_project": "Build a RESTful Course & Lesson Management API"
+ },
+ {
+ "step_number": 3,
+ "title": "Module 3: Authentication, Security & Middleware",
+ "duration": "1.5 weeks (10 hours)",
+ "summary": "Protect resources with industry-standard tokens and validation.",
+ "topics": ["JWT creation, verification, and expiry", "Password hashing with bcrypt", "CORS, Helmet, Rate Limiting, and Joi/Zod validation"],
+ "hands_on_project": "Build a Secure Role-Based Auth Service (Learner, Teacher, Admin)"
+ },
+ {
+ "step_number": 4,
+ "title": "Module 4: Database Integration & Production Deployment",
+ "duration": "1 week (7 hours)",
+ "summary": "Connect to databases, handle transactions, and deploy services.",
+ "topics": ["Connecting SQLite/PostgreSQL with queries", "Migration scripts & seeders", "Containerization with Docker & cloud deploy"],
+ "hands_on_project": "Capstone: Collaborative Note & Bookmark API with Full CRUD and JWT"
+ }
+ ],
+ "key_skills_acquired": [
+ "Designing robust RESTful web APIs",
+ "Implementing JWT authentication and authorization guards",
+ "Middleware creation for logging, validation, and error trapping",
+ "Relational database integration and query execution",
+ "Backend security practices (sanitization, rate limits, CORS)"
+ ],
+ "capstone_project": {
+ "title": "LearnSphere Content & Review REST API",
+ "description": "A production backend providing endpoints for users, course modules, review submissions, and real-time validation with rate limits.",
+ "skills_demonstrated": ["Node.js", "Express", "SQLite", "JWT Auth", "Input Validation"]
+ }
+ },
+ {
+ "id": "course-python-mastery",
+ "title": "Python Programming & Algorithmic Problem Solving",
+ "topic": "Python",
+ "slug": "python-programming-mastery",
+ "difficulty": "Beginner",
+ "description": "Learn clean, idiomatic Python from scratch. Cover control flow, built-in data structures, object-oriented design, file processing, and algorithmic problem-solving used in web backends and data science.",
+ "prerequisites": ["None - designed for complete beginners and programmers transitioning to Python"],
+ "estimated_duration": "4 weeks (5 hours/week)",
+ "total_hours": 20,
+ "resource_types": ["Interactive Lab", "Video Lectures", "Project Guided", "Documentation & Cheatsheets", "Assessment Quiz"],
+ "target_roles": ["Python Developer", "Software Engineer", "Data Analyst", "Backend Engineer"],
+ "topics_covered": [
+ "Python Basics: Variables, Operators, Dynamic Typing",
+ "Control Structures: Conditionals, Loops, and Iterators",
+ "Collections: Lists, Tuples, Dictionaries, and Sets",
+ "List, Dict, and Generator Comprehensions",
+ "Object-Oriented Programming: Classes, Objects, Inheritance, Magic Methods",
+ "File Handling, JSON, and CSV processing",
+ "Exception Handling and Robust Error Management",
+ "Virtual Environments, Pip, and Code Structuring"
+ ],
+ "learning_sequence": [
+ {
+ "step_number": 1,
+ "title": "Module 1: Python Fundamentals & Logic",
+ "duration": "1 week (5 hours)",
+ "summary": "Learn the syntax, interactive shell, and core flow control.",
+ "topics": ["Variables & Type casting", "If-elif-else statements", "While and For loops with range"],
+ "hands_on_project": "Build an Interactive Text-based Grade Calculator & Stats Analyzer"
+ },
+ {
+ "step_number": 2,
+ "title": "Module 2: Mastering Python Data Structures",
+ "duration": "1 week (5 hours)",
+ "summary": "Harness the power of Python's built-in dictionaries, sets, and lists.",
+ "topics": ["List slicing & sorting", "Dictionary key-value lookups & hashing", "Set operations & comprehensions"],
+ "hands_on_project": "Build a Word Frequency & Text Summary Tool"
+ },
+ {
+ "step_number": 3,
+ "title": "Module 3: Object-Oriented Python Architecture",
+ "duration": "1 week (5 hours)",
+ "summary": "Structure programs using classes, encapsulation, and inheritance.",
+ "topics": ["Class definitions & __init__ constructor", "Instance vs Class variables", "Inheritance, Polymorphism & dunder methods (__str__, __repr__)"],
+ "hands_on_project": "Build a Bank Account & Transaction Ledger Simulation System"
+ },
+ {
+ "step_number": 4,
+ "title": "Module 4: File I/O, Exceptions & Capstone Project",
+ "duration": "1 week (5 hours)",
+ "summary": "Read, process, and write files while handling edge cases safely.",
+ "topics": ["Reading & writing JSON and CSV files", "Try-except-finally blocks", "Structuring Python modules and packages"],
+ "hands_on_project": "Capstone: Automated Student Gradebook & CSV Exporter Tool"
+ }
+ ],
+ "key_skills_acquired": [
+ "Writing idiomatic and PEP 8 compliant Python code",
+ "Manipulating complex data structures with comprehensions",
+ "Object-oriented design and modular program organization",
+ "Robust error handling and file system operations",
+ "Foundational coding for backend and machine learning pipelines"
+ ],
+ "capstone_project": {
+ "title": "CLI Student Performance & Report Card Generator",
+ "description": "A command-line application that ingests student test scores from CSV/JSON, calculates percentiles, identifies learning gaps, and generates formatted reports.",
+ "skills_demonstrated": ["OOP", "Data Structures", "File I/O", "Exception Handling"]
+ }
+ },
+ {
+ "id": "course-java-springboot",
+ "title": "Java: Object-Oriented Engineering & Spring Boot Microservices",
+ "topic": "Java",
+ "slug": "java-oop-springboot",
+ "difficulty": "Intermediate",
+ "description": "Build industrial-strength enterprise applications with Java 21, object-oriented design patterns, the Java Collections Framework, and production-ready Spring Boot microservices.",
+ "prerequisites": ["Basic programming knowledge in any language", "Familiarity with basic data structures"],
+ "estimated_duration": "6 weeks (6-8 hours/week)",
+ "total_hours": 42,
+ "resource_types": ["Interactive Lab", "Video Lectures", "Project Guided", "Documentation & Cheatsheets", "Assessment Quiz"],
+ "target_roles": ["Java Developer", "Enterprise Backend Engineer", "Software Engineer"],
+ "topics_covered": [
+ "Java Syntax, JVM, JRE, and Bytecode Compilation",
+ "Core OOP: Encapsulation, Inheritance, Polymorphism, Abstraction",
+ "Interfaces, Abstract Classes, and Functional Interfaces",
+ "Java Collections Framework (List, Set, Map) and Generics",
+ "Java Streams API and Lambda Expressions",
+ "Exception Handling and Multi-threading Basics",
+ "Maven Project Management and Unit Testing with JUnit 5",
+ "Spring Boot 3: Dependency Injection, Controllers, Services, and Spring Data JPA"
+ ],
+ "learning_sequence": [
+ {
+ "step_number": 1,
+ "title": "Module 1: Java Foundations & Type Safety",
+ "duration": "1 week (6 hours)",
+ "summary": "Understand strong static typing and JVM compilation.",
+ "topics": ["Primitive types vs Reference wrappers", "Methods, parameters, and return types", "Memory management & Garbage collection basics"],
+ "hands_on_project": "Build an Inventory Item Tracking Console Application"
+ },
+ {
+ "step_number": 2,
+ "title": "Module 2: Object-Oriented Architecture & Clean Design",
+ "duration": "1.5 weeks (10 hours)",
+ "summary": "Apply SOLID principles and object composition in Java.",
+ "topics": ["Classes, Constructors & Encapsulation", "Inheritance & Polymorphic dispatch", "Interfaces & Abstract classes"],
+ "hands_on_project": "Build a Vehicle Rental & Pricing Simulation Engine"
+ },
+ {
+ "step_number": 3,
+ "title": "Module 3: Collections Framework & Stream API",
+ "duration": "1 week (8 hours)",
+ "summary": "Master high-performance data processing using Streams.",
+ "topics": ["ArrayList, LinkedList, HashSet, HashMap", "Generics and type boundaries", "Lambda expressions, filter, map, and collect"],
+ "hands_on_project": "Build a Transaction Log Processor with Stream Analytics"
+ },
+ {
+ "step_number": 4,
+ "title": "Module 4: Spring Boot Core & Dependency Injection",
+ "duration": "1.5 weeks (10 hours)",
+ "summary": "Build modern backend services with the Spring framework.",
+ "topics": ["Spring Boot starter dependencies & Maven", "@RestController, @Service, and @Repository annotations", "Inversion of Control & @Autowired"],
+ "hands_on_project": "Build a RESTful Student Registration API with Spring Boot"
+ },
+ {
+ "step_number": 5,
+ "title": "Module 5: Spring Data JPA & Capstone Microservice",
+ "duration": "1 week (8 hours)",
+ "summary": "Persist entities to SQL databases and deploy.",
+ "topics": ["Entity relationships (@OneToMany, @ManyToOne)", "Spring Data JPA Repository interfaces", "Testing endpoints with MockMvc"],
+ "hands_on_project": "Capstone: Hospital Appointment Booking Microservice"
+ }
+ ],
+ "key_skills_acquired": [
+ "Mastery of Java 17/21 type system and JVM mechanics",
+ "Designing complex systems with SOLID design patterns",
+ "Declarative stream processing and generic data collections",
+ "Building enterprise RESTful microservices with Spring Boot",
+ "ORM persistence with Spring Data JPA and Hibernate"
+ ],
+ "capstone_project": {
+ "title": "Course Enrollment & Grading Microservice",
+ "description": "An enterprise-grade Spring Boot microservice handling student registrations, course prerequisite verification, grade submissions, and JPA persistence.",
+ "skills_demonstrated": ["Java 21", "Spring Boot 3", "Spring Data JPA", "H2/PostgreSQL", "JUnit 5"]
+ }
+ },
+ {
+ "id": "course-sql-database",
+ "title": "SQL & Relational Database Architecture",
+ "topic": "SQL",
+ "slug": "sql-database-mastery",
+ "difficulty": "Beginner",
+ "description": "Design efficient relational database schemas, write complex analytical SQL queries, optimize joins, understand ACID transactions, and build scalable data backends for web applications.",
+ "prerequisites": ["No prior database experience required"],
+ "estimated_duration": "4 weeks (4-5 hours/week)",
+ "total_hours": 18,
+ "resource_types": ["Interactive Lab", "Video Lectures", "Project Guided", "Documentation & Cheatsheets", "Assessment Quiz"],
+ "target_roles": ["Backend Developer", "Data Analyst", "Database Administrator", "Full Stack Developer"],
+ "topics_covered": [
+ "Relational Database Concepts and Entity Relationship (ER) Diagrams",
+ "Data Definition Language (DDL): CREATE, ALTER, DROP, Constraints",
+ "Data Manipulation Language (DML): INSERT, UPDATE, DELETE, SELECT",
+ "Filtering, Sorting, and Aggregations (WHERE, ORDER BY, GROUP BY, HAVING)",
+ "Multi-Table Relational Joins (INNER, LEFT, RIGHT, FULL OUTER, CROSS)",
+ "Subqueries, Common Table Expressions (CTEs), and Window Functions",
+ "Indexes, EXPLAIN Query Plans, and Performance Optimization",
+ "Database Transactions, Locks, and ACID Guarantees"
+ ],
+ "learning_sequence": [
+ {
+ "step_number": 1,
+ "title": "Module 1: Relational Modeling & Essential SQL Queries",
+ "duration": "1 week (4 hours)",
+ "summary": "Understand tables, foreign keys, and basic querying.",
+ "topics": ["Primary keys, foreign keys & unique constraints", "SELECT statements, WHERE filters, and string matching (LIKE)", "Sorting with ORDER BY and LIMIT pagination"],
+ "hands_on_project": "Create a School Library Schema & Run Inventory Search Queries"
+ },
+ {
+ "step_number": 2,
+ "title": "Module 2: Aggregations, Grouping & Multi-Table Joins",
+ "duration": "1 week (5 hours)",
+ "summary": "Combine multiple normalized tables and summarize trends.",
+ "topics": ["COUNT, SUM, AVG, MIN, MAX aggregations", "GROUP BY and HAVING filtering", "INNER JOIN vs LEFT/RIGHT JOIN nuances"],
+ "hands_on_project": "Build an E-Commerce Sales Reporting Query Suite"
+ },
+ {
+ "step_number": 3,
+ "title": "Module 3: Advanced SQL: CTEs, Subqueries & Window Functions",
+ "duration": "1 week (5 hours)",
+ "summary": "Write complex analytical queries without slow client-side loops.",
+ "topics": ["Correlated subqueries and EXISTS clauses", "Common Table Expressions (WITH syntax)", "Window functions: ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD"],
+ "hands_on_project": "Build a Monthly User Churn & Cohort Retention Query Dashboard"
+ },
+ {
+ "step_number": 4,
+ "title": "Module 4: Indexing, Transactions & Capstone Schema Design",
+ "duration": "1 week (4 hours)",
+ "summary": "Optimize query execution time and ensure data integrity.",
+ "topics": ["B-Tree indexes and composite indexes", "Analyzing queries with EXPLAIN", "Transactions: BEGIN, COMMIT, ROLLBACK and ACID properties"],
+ "hands_on_project": "Capstone: Multi-Tenant SaaS Billing & Subscription Database Schema"
+ }
+ ],
+ "key_skills_acquired": [
+ "Designing normalized 3NF database schemas",
+ "Writing performant multi-table joins and analytical CTEs",
+ "Window functions for time-series and cohort ranking",
+ "Index profiling and execution plan optimization",
+ "Transaction management for mission-critical operations"
+ ],
+ "capstone_project": {
+ "title": "Production E-Commerce Schema & Analytics Engine",
+ "description": "A comprehensive database schema for customers, orders, inventory, and payments with optimized indexes and an analytics query suite.",
+ "skills_demonstrated": ["Schema Design", "Complex Joins", "Window Functions", "Index Optimization"]
+ }
+ },
+ {
+ "id": "course-ml-foundations",
+ "title": "Machine Learning Foundations & Applied Scikit-Learn",
+ "topic": "Machine Learning",
+ "slug": "machine-learning-foundations",
+ "difficulty": "Intermediate",
+ "description": "Learn the mathematics, data preprocessing, and algorithm implementations behind modern machine learning. Train, evaluate, and tune supervised and unsupervised models using Python, NumPy, Pandas, and Scikit-Learn.",
+ "prerequisites": ["Python programming proficiency", "Basic algebra and introductory statistics"],
+ "estimated_duration": "6 weeks (6-8 hours/week)",
+ "total_hours": 44,
+ "resource_types": ["Interactive Lab", "Video Lectures", "Project Guided", "Documentation & Cheatsheets", "Assessment Quiz"],
+ "target_roles": ["Machine Learning Engineer", "Data Scientist", "AI Engineer", "Data Analyst"],
+ "topics_covered": [
+ "Machine Learning Workflow: Problem Formulation & Validation Splits",
+ "Data Manipulation with Pandas & Numerical Computing with NumPy",
+ "Feature Engineering, Imputation, One-Hot Encoding, and Scaling",
+ "Supervised Learning: Linear Regression, Logistic Regression",
+ "Tree-based Models: Decision Trees, Random Forests, Gradient Boosting",
+ "Model Evaluation Metrics: Precision, Recall, F1-Score, ROC-AUC, Confusion Matrix",
+ "Hyperparameter Tuning with GridSearchCV and Cross-Validation",
+ "Unsupervised Learning: K-Means Clustering and Dimensionality Reduction (PCA)",
+ "Deploying Models as REST APIs with FastAPI"
+ ],
+ "learning_sequence": [
+ {
+ "step_number": 1,
+ "title": "Module 1: Data Preparation, Pandas & Exploratory Data Analysis (EDA)",
+ "duration": "1 week (6 hours)",
+ "summary": "Clean and preprocess raw datasets for machine learning models.",
+ "topics": ["Pandas DataFrame transformations", "Handling missing values and outliers", "Feature scaling with StandardScaler and MinMaxScaler"],
+ "hands_on_project": "Build an Automated EDA Pipeline for Housing Price Data"
+ },
+ {
+ "step_number": 2,
+ "title": "Module 2: Supervised Learning: Regression & Classification",
+ "duration": "1.5 weeks (10 hours)",
+ "summary": "Train predictive models for continuous and categorical targets.",
+ "topics": ["Cost functions & Gradient descent intuition", "Linear Regression for price prediction", "Logistic Regression for binary classification"],
+ "hands_on_project": "Build a Diabetes Risk Prediction Classifier"
+ },
+ {
+ "step_number": 3,
+ "title": "Module 3: Ensemble Methods & Hyperparameter Optimization",
+ "duration": "1.5 weeks (10 hours)",
+ "summary": "Boost predictive accuracy using Random Forests and cross-validation.",
+ "topics": ["Decision tree splitting criteria (Gini, Entropy)", "Random Forests & Feature Importance", "GridSearchCV and RandomizedSearchCV for tuning"],
+ "hands_on_project": "Build a Customer Churn Predictor with 85%+ ROC-AUC"
+ },
+ {
+ "step_number": 4,
+ "title": "Module 4: Unsupervised Learning & Clustering",
+ "duration": "1 week (8 hours)",
+ "summary": "Discover hidden structures in unlabeled data.",
+ "topics": ["K-Means clustering and Elbow method", "Principal Component Analysis (PCA) for visualization", "Anomaly detection fundamentals"],
+ "hands_on_project": "Customer Segmentation & Behavioral Clustering for Retailers"
+ },
+ {
+ "step_number": 5,
+ "title": "Module 5: Model Deployment & Live API Serving",
+ "duration": "1 week (10 hours)",
+ "summary": "Export trained models and serve them through high-speed APIs.",
+ "topics": ["Model serialization with joblib/pickle", "FastAPI inference endpoint creation", "Input data validation with Pydantic"],
+ "hands_on_project": "Capstone: End-to-End Loan Default Prediction Service with Live API"
+ }
+ ],
+ "key_skills_acquired": [
+ "End-to-end data preprocessing and feature pipeline construction",
+ "Selecting and training appropriate ML algorithms for business tasks",
+ "Rigorous model evaluation and overfitting prevention",
+ "Dimensionality reduction and customer segmentation",
+ "Exporting and deploying machine learning models into production APIs"
+ ],
+ "capstone_project": {
+ "title": "Production Credit Default Predictor & FastAPI Service",
+ "description": "A trained gradient boosted classification pipeline saved with joblib, wrapped in a FastAPI endpoint that receives applicant parameters and returns risk scores with confidence intervals.",
+ "skills_demonstrated": ["Scikit-Learn", "Feature Pipelines", "FastAPI", "ROC-AUC Analysis"]
+ }
+ },
+ {
+ "id": "course-fullstack-capstone",
+ "title": "Full Stack Web Development: Modern Web Applications",
+ "topic": "Full Stack Web Development",
+ "slug": "full-stack-web-development",
+ "difficulty": "Advanced",
+ "description": "Integrate React, Node.js, Express, and SQL into a unified, production-ready full-stack application featuring authentication, state management, REST APIs, automated testing, and CI/CD deployment.",
+ "prerequisites": ["React fundamentals", "Node.js & Express fundamentals", "Basic SQL querying"],
+ "estimated_duration": "6 weeks (8-10 hours/week)",
+ "total_hours": 50,
+ "resource_types": ["Interactive Lab", "Video Lectures", "Project Guided", "Documentation & Cheatsheets", "Assessment Quiz"],
+ "target_roles": ["Full Stack Developer", "Software Engineer", "Web Application Developer"],
+ "topics_covered": [
+ "Full Stack Architecture & Monorepo / Split Repo Workflows",
+ "End-to-End Authentication: Cookies, JWT, and Session Lifecycle",
+ "RESTful API Contract Design and Schema Sharing",
+ "State Management on Frontend: Context API & Optimistic Updates",
+ "Database Schema Design, Migrations & Transactions",
+ "File Uploads, Cloud Storage & Asset Optimization",
+ "Automated Testing: Unit (Vitest/Jest) and Integration Testing (Supertest)",
+ "Containerization with Docker & Cloud Deployment (Vercel + Render)"
+ ],
+ "learning_sequence": [
+ {
+ "step_number": 1,
+ "title": "Module 1: Full-Stack System Design & Project Scaffolding",
+ "duration": "1 week (8 hours)",
+ "summary": "Plan database entities, API endpoints, and clean client-server architecture.",
+ "topics": ["Client-server separation & CORS configuration", "Environment variable management across tiers", "Standardizing JSON error contracts"],
+ "hands_on_project": "Scaffold Monorepo with Vite Frontend and Express Backend"
+ },
+ {
+ "step_number": 2,
+ "title": "Module 2: Complete Authentication & User Sessions",
+ "duration": "1.5 weeks (12 hours)",
+ "summary": "Implement secure sign-up, sign-in, token refresh, and password recovery.",
+ "topics": ["HttpOnly cookies vs LocalStorage tokens", "Route guards on frontend and auth middleware on backend", "Role-based authorization checks"],
+ "hands_on_project": "Build an End-to-End Authentication Module with Protected Dashboards"
+ },
+ {
+ "step_number": 3,
+ "title": "Module 3: Core Business Logic, Data Persistence & CRUD",
+ "duration": "1.5 weeks (14 hours)",
+ "summary": "Connect frontend components directly to database queries.",
+ "topics": ["Pagination, filtering, and search across tiers", "Optimistic UI updates for snappy responsiveness", "Handling concurrency and transaction rollbacks"],
+ "hands_on_project": "Build a Collaborative Course Review & Rating Platform"
+ },
+ {
+ "step_number": 4,
+ "title": "Module 4: Testing, Dockerizing & Production Deployment",
+ "duration": "2 weeks (16 hours)",
+ "summary": "Test all components, package into Docker, and deploy live.",
+ "topics": ["End-to-end API testing with Supertest", "Writing Dockerfiles for frontend & backend", "Deploying with CI/CD GitHub Actions"],
+ "hands_on_project": "Capstone: Launch the Complete LearnSphere Community Platform Live"
+ }
+ ],
+ "key_skills_acquired": [
+ "Connecting React frontends to Node.js backend services seamlessly",
+ "Architecting secure token-based user authentication systems",
+ "Designing resilient database models and transactions",
+ "Writing automated tests across backend APIs and frontend UI",
+ "Building and deploying containerized full-stack web applications"
+ ],
+ "capstone_project": {
+ "title": "LearnSphere Collaborative Learning Platform",
+ "description": "A complete full-stack web application featuring user authentication, course enrollment, interactive roadmaps, real-time reviews, and persistent progress tracking.",
+ "skills_demonstrated": ["React", "Node.js", "Express", "SQLite", "Docker", "REST APIs"]
+ }
+ }
+]
diff --git a/backend/test_backend.py b/backend/test_backend.py
new file mode 100644
index 0000000..9ac2988
--- /dev/null
+++ b/backend/test_backend.py
@@ -0,0 +1,147 @@
+import sys
+from pathlib import Path
+
+# Add project root to sys.path
+sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
+
+import json
+import pytest
+from fastapi.testclient import TestClient
+
+from backend.main import app
+from backend.database import init_db, get_all_courses, get_course_by_id
+
+client = TestClient(app)
+
+
+def setup_module():
+ init_db()
+
+
+def test_health_endpoint():
+ response = client.get("/health")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["status"] == "ok"
+ assert data["service"] == "learnsphere-ai-assistant"
+
+
+def test_get_all_courses():
+ response = client.get("/api/ai/content")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["success"] is True
+ assert data["total"] == 8
+ topics = [c["topic"] for c in data["courses"]]
+ assert "JavaScript" in topics
+ assert "React" in topics
+ assert "Python" in topics
+ assert "Machine Learning" in topics
+
+
+def test_filter_courses_by_topic():
+ response = client.get("/api/ai/content?topic=React")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["total"] >= 1
+ assert data["courses"][0]["topic"] == "React"
+
+
+def test_filter_courses_by_difficulty():
+ response = client.get("/api/ai/content?difficulty=Beginner")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["total"] >= 1
+ for c in data["courses"]:
+ assert c["difficulty"] == "Beginner"
+
+
+def test_get_course_detail():
+ response = client.get("/api/ai/content/course-react-engineering")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["success"] is True
+ course = data["course"]
+ assert course["id"] == "course-react-engineering"
+ assert len(course["learning_sequence"]) >= 4
+ assert len(course["topics_covered"]) >= 5
+ assert "capstone_project" in course
+
+
+def test_get_invalid_course_returns_404():
+ response = client.get("/api/ai/content/non-existent-course-id")
+ assert response.status_code == 404
+
+
+def test_recommendation_javascript_to_react():
+ # Primary user requirement test case
+ payload = {
+ "query": "I know basic JavaScript and want to become a React developer in 3 months.",
+ "current_level": "auto",
+ "target_timeframe": "3 months"
+ }
+ response = client.post("/api/ai/recommend", json=payload)
+ assert response.status_code == 200
+ data = response.json()
+ assert data["success"] is True
+ assert data["query_id"].startswith("ls-query-")
+
+ profile = data["learner_profile"]
+ assert "React" in profile["target_role"]
+ assert profile["current_level"] in ["Intermediate", "Beginner"]
+ assert "3 months" in profile["timeframe"]
+
+ rec_titles = [c["title"] for c in data["recommended_courses"]]
+ # Should include Modern JavaScript bridge and React
+ assert any("JavaScript" in t for t in rec_titles)
+ assert any("React" in t for t in rec_titles)
+
+ # Learning sequence should have phases
+ seq = data["learning_sequence"]
+ assert len(seq) >= 2
+ assert seq[0]["phase_number"] == 1
+ assert "JavaScript" in seq[0]["course_title"]
+ assert "React" in seq[1]["course_title"]
+
+ assert len(data["immediate_next_step"]) > 20
+ assert len(data["why_recommended"]) > 20
+
+
+def test_recommendation_beginner_machine_learning():
+ payload = {
+ "query": "I am a complete beginner and want to learn Machine Learning in 4 months."
+ }
+ response = client.post("/api/ai/recommend", json=payload)
+ assert response.status_code == 200
+ data = response.json()
+ rec_titles = [c["title"] for c in data["recommended_courses"]]
+ # Should recommend Python first as prerequisite, then ML
+ assert any("Python" in t for t in rec_titles)
+ assert any("Machine Learning" in t for t in rec_titles)
+
+
+def test_empty_query_returns_400():
+ response = client.post("/api/ai/recommend", json={"query": " "})
+ assert response.status_code in [400, 422]
+
+
+def test_followup_chat():
+ payload = {
+ "message": "What portfolio project should I build after finishing the React course?",
+ "session_id": "test-session-123"
+ }
+ response = client.post("/api/ai/chat", json=payload)
+ assert response.status_code == 200
+ data = response.json()
+ assert data["session_id"] == "test-session-123"
+ assert len(data["reply"]) > 10
+ assert len(data["suggested_prompts"]) > 0
+
+
+def test_catalog_stats():
+ response = client.get("/api/ai/stats")
+ assert response.status_code == 200
+ data = response.json()
+ assert data["total_courses"] == 8
+ assert data["total_curriculum_hours"] > 200
+ assert data["total_learning_modules"] >= 30
diff --git a/chatbot.py b/chatbot.py
index 239cfa3..28c6cc3 100644
--- a/chatbot.py
+++ b/chatbot.py
@@ -1,234 +1,13 @@
+"""
+chatbot.py - Entrypoint for LearnSphere AI Learning Assistant & AI Tutor Services.
+Runs the upgraded FastAPI backend on port 5000.
+"""
import os
-import time
-import html
-from collections import defaultdict, deque
-
-from flask import Flask, request, jsonify, render_template
-from flask_cors import CORS
-from dotenv import load_dotenv
-
-# Load the .env file before reading any environment variables.
-load_dotenv()
-
-# -----------------------------
-# App setup
-# -----------------------------
-app = Flask(__name__, template_folder="templates", static_folder="static")
-CORS(app)
-
-# Auth runs on the Node.js server (server/index.js). This app is AI tutor only.
-
-# -----------------------------
-# Security: API key via env var
-# -----------------------------
-API_KEY = os.environ.get("GEMINI_API_KEY")
-client = None
-MODEL_NAME = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
-
-if API_KEY:
- from google import genai
-
- client = genai.Client(api_key=API_KEY)
-
-
-# -----------------------------
-# Input/output hardening
-# -----------------------------
-MAX_MESSAGE_CHARS = int(os.environ.get("CHAT_MAX_MESSAGE_CHARS", "2000"))
-
-ABUSE_KEYWORDS = [
- "ignore previous",
- "disregard",
- "system prompt",
- "developer prompt",
- "jailbreak",
- "act as",
- "bypass",
- "reveal your instructions",
- "how to build a bomb",
- "make a bomb",
- "poison",
- "suicide",
- "self-harm",
- "kill yourself",
- "kill someone",
- "murder",
- "steal",
- "credit card",
- "creditcard",
-]
-
-
-def format_response(text: str) -> str:
- """Return safe text for the frontend (no unsafe HTML)."""
- if text is None:
- return ""
- return html.escape(text).replace("\n", " ").strip()
-
-
-def looks_like_abuse(user_text: str) -> bool:
- t = (user_text or "").lower()
- return any(k in t for k in ABUSE_KEYWORDS)
-
-
-def generate_reply(prompt: str, fallback: str) -> str:
- """Call Gemini and return plain text, or `fallback` if no usable output."""
- if client is None:
- return fallback
-
- response = client.models.generate_content(model=MODEL_NAME, contents=prompt)
- text = (response.text or "").strip() if response is not None else ""
- return text if text else fallback
-
-
-# -----------------------------
-# Rate limiting (per IP)
-# -----------------------------
-RATE_LIMIT_WINDOW_SECONDS = int(os.environ.get("CHAT_RATE_LIMIT_WINDOW_SECONDS", "60"))
-RATE_LIMIT_COUNT = int(os.environ.get("CHAT_RATE_LIMIT_COUNT", "10"))
-
-_requests = defaultdict(deque)
-
-
-def rate_limited(ip: str) -> bool:
- now = time.time()
- q = _requests[ip]
-
- while q and (now - q[0]) > RATE_LIMIT_WINDOW_SECONDS:
- q.popleft()
-
- if len(q) >= RATE_LIMIT_COUNT:
- return True
-
- q.append(now)
- return False
-
-
-@app.route("/")
-def home():
- return render_template("index.html")
-
-
-@app.route("/chat", methods=["POST"])
-def chat():
- if client is None:
- return jsonify({"error": "AI tutor is not configured (GEMINI_API_KEY missing)"}), 503
-
- data = request.get_json(silent=True)
-
- if not data or "message" not in data:
- return jsonify({"error": "No message provided"}), 400
-
- user_input = data["message"]
-
- if not isinstance(user_input, str):
- return jsonify({"error": "Message must be a string"}), 400
-
- user_input = user_input.strip()
-
- if not user_input:
- return jsonify({"error": "Message cannot be empty"}), 400
-
- if len(user_input) > MAX_MESSAGE_CHARS:
- return (
- jsonify({"error": f"Message too large (max {MAX_MESSAGE_CHARS} characters)."}),
- 413,
- )
-
- ip = request.headers.get("X-Forwarded-For", request.remote_addr) or "unknown"
- ip = ip.split(",")[0].strip()
-
- if rate_limited(ip):
- return jsonify({"error": "Too many requests. Please try again later."}), 429
-
- if looks_like_abuse(user_input):
- reply = (
- "I can't help with that request. "
- "If you need help with a physics, maths, chemistry, or biology topic, tell me what concept you're working on."
- )
- return jsonify({"reply": format_response(reply)})
-
- try:
- system_guardrails = (
- "You are an educational tutor for Physics, Maths, Chemistry, and Biology. "
- "Be concise, step-by-step when useful, and avoid unsafe or illegal content. "
- "If the user asks for something unrelated, politely guide them back to learning topics."
- )
-
- prompt = f"{system_guardrails}\n\nUser message: {user_input}"
-
- reply_text = generate_reply(
- prompt,
- "I'm not sure how to respond to that. Can you try rephrasing?",
- )
-
- return jsonify({"reply": format_response(reply_text)})
-
- except Exception as e:
- print(f"[ERROR] Chatbot error: {e}")
- return (
- jsonify({"reply": "An error occurred while processing your request. Please try again later."}),
- 500,
- )
-
-
-@app.route("/explain_mistake", methods=["POST"])
-def explain_mistake():
- if client is None:
- return jsonify({"error": "AI tutor is not configured (GEMINI_API_KEY missing)"}), 503
-
- data = request.get_json(silent=True)
-
- if not data:
- return jsonify({"error": "No data provided"}), 400
-
- question = data.get("question", "").strip()
- learner_answer = data.get("learnerAnswer", "").strip()
- correct_answer = data.get("correctAnswer", "").strip()
- topic = data.get("topic", "general").strip()
-
- if not question or not learner_answer or not correct_answer:
- return jsonify({"error": "Missing required fields (question, learnerAnswer, correctAnswer)"}), 400
-
- ip = request.headers.get("X-Forwarded-For", request.remote_addr) or "unknown"
- ip = ip.split(",")[0].strip()
-
- if rate_limited(ip):
- return jsonify({"error": "Too many requests. Please try again later."}), 429
-
- try:
- system_prompt = (
- "You are an educational tutor for Physics, Maths, Chemistry, and Biology. "
- "A learner made a mistake on a quiz. Please provide a targeted explanation including:\n"
- "1. Why the selected answer is incorrect.\n"
- "2. What core concept this maps to, explained simply.\n"
- "3. One follow-up practice question (multiple choice) to test their understanding, "
- "with a clearly indicated answer key/rubric."
- )
-
- prompt = (
- f"{system_prompt}\n\n"
- f"Topic: {topic}\n"
- f"Question Text: {question}\n"
- f"Learner's Selected Answer: {learner_answer}\n"
- f"Correct Answer: {correct_answer}"
- )
-
- reply_text = generate_reply(
- prompt,
- "I'm not sure how to explain this mistake. Let's review the main concept together.",
- )
-
- return jsonify({"reply": format_response(reply_text)})
-
- except Exception as e:
- print(f"[ERROR] Mistake explanation error: {e}")
- return (
- jsonify({"reply": "An error occurred while generating the explanation. Please try again later."}),
- 500,
- )
-
+import uvicorn
+from backend.main import app
if __name__ == "__main__":
+ port = int(os.environ.get("PORT", "5000"))
debug_mode = os.environ.get("FLASK_DEBUG", "false").lower() == "true"
- app.run(host="0.0.0.0", port=5000, debug=debug_mode)
+ print(f"Starting LearnSphere AI Learning Assistant on http://127.0.0.1:{port} ...")
+ uvicorn.run("backend.main:app", host="0.0.0.0", port=port, reload=debug_mode)
diff --git a/courses.html b/courses.html
index fc7c076..f7fae23 100644
--- a/courses.html
+++ b/courses.html
@@ -29,6 +29,7 @@