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. +

+
+ + + + + +
+
+ + + +
+ Quick-start goal prompts (click to try): +
+
+ + + + + +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + Tip: Press Ctrl+Enter to submit + + +
+
+ + +
+ +

Crafting Your Custom Learning Path

+
Analyzing your career ambition and skill profile...
+
+ + + + + +
+ +

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) +

+
+
+ + + + + +
+
+

+ + Interactive Learning Roadmap (DAG Graph) +

+ + +
+ + +
+
+ +

+ 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: +

    + +
    + +
    + +
    + + +
    +
    +
    +
    + + +
    +

    + + 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! +

    + +
    + + + + +
    + +
    +
    + + + +
    + + + + + + + + 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.phase_title)}
    +
    ~${phase.duration_weeks} weeks
    +
    +
    ${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}
    + + +
    ${modulesList}
    + + ${c.capstone_project && c.capstone_project.title ? ` +
    + Capstone: ${escapeHtml(c.capstone_project.title)} โ€” ${escapeHtml(c.capstone_project.description)} +
    + ` : ""} +
    + +
    + +
    + `; + + // 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.title || "Strategic Fit")}
    +
    +
    ${escapeHtml(pt.description || "")}
    +
    + `).join(""); + } else if (data.why_recommended) { + takeawaysGrid.innerHTML = ` +
    +
    +
    +
    Strategic Rationale & Pacing
    +
    +
    ${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 = ` +
    +
    + Milestone #${stepNum} + ${escapeHtml(node.category || "Core")} +
    +
    + ${initialStatus} +
    +
    + +
    ${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 => ` + + `).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"} +
    +
    + + `; + + 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 @@