├── backend ├── assistants.json ├── .DS_Store ├── dump.rdb ├── api │ ├── __init__.py │ ├── user.py │ ├── routes.py │ ├── in_class.py │ └── webhook.py ├── audio_cache │ └── output.mp3 ├── __pycache__ │ ├── chatbot.cpython-313.pyc │ ├── context_manager.cpython-313.pyc │ └── s3_context_manager.cpython-313.pyc ├── utils │ ├── __pycache__ │ │ ├── s3_utils.cpython-313.pyc │ │ ├── user_utils.cpython-313.pyc │ │ ├── socket_utils.cpython-313.pyc │ │ ├── course_manager.cpython-313.pyc │ │ ├── firebase_admin.cpython-313.pyc │ │ └── load_and_process_index.cpython-313.pyc │ ├── socket_utils.py │ ├── firebase_admin.py │ ├── user_utils.py │ └── load_and_process_index.py ├── functions │ ├── __pycache__ │ │ ├── slides_navigation.cpython-313.pyc │ │ └── get_detailed_content.cpython-313.pyc │ ├── get_detailed_content.py │ └── slides_navigation.py ├── user_files_utils.py ├── routes │ ├── delete_routes.py │ ├── delete_course_routes.py │ ├── upload_routes.py │ ├── course_info_routes.py │ ├── voice_routes.py │ └── aiTutor_routes.py └── app.py ├── app ├── vpi-test │ └── [title] │ │ └── page.tsx ├── favicon.ico ├── fonts │ ├── GeistVF.woff │ └── GeistMonoVF.woff ├── page.tsx ├── my-publish │ ├── page.tsx │ ├── data-center │ │ └── page.tsx │ ├── publish-details │ │ └── page.tsx │ ├── my-channel │ │ └── page.tsx │ ├── help-center │ │ └── page.tsx │ └── monetization-center │ │ └── page.tsx ├── courses │ ├── layout.tsx │ ├── [id] │ │ ├── types.ts │ │ ├── components │ │ │ ├── SlideViewer.tsx │ │ │ └── ChatHistory.tsx │ │ ├── hooks │ │ │ ├── useConversation.ts │ │ │ └── useSpeech.ts │ │ └── page.tsx │ └── page.tsx ├── dashboard │ └── layout.tsx ├── services │ └── chatService.ts ├── layout.tsx ├── welcome │ └── page.tsx ├── my-uploads │ └── page.tsx ├── course-preview │ └── [id] │ │ └── page.tsx ├── ai-tutor │ └── page.tsx ├── schedule │ └── page.tsx ├── globals.css ├── my-courses │ └── page.tsx └── vapi-test │ ├── page.tsx │ └── [title] │ └── page.tsx ├── next.config.mjs ├── postcss.config.mjs ├── next-env.d.ts ├── .gitignore ├── package.json ├── tsconfig.json ├── middleware.ts ├── tailwind.config.ts └── README.md /backend/assistants.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /app/vpi-test/[title]/page.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobhaotian/speech-driven-lessons/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /backend/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobhaotian/speech-driven-lessons/HEAD/backend/.DS_Store -------------------------------------------------------------------------------- /backend/dump.rdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobhaotian/speech-driven-lessons/HEAD/backend/dump.rdb -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobhaotian/speech-driven-lessons/HEAD/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobhaotian/speech-driven-lessons/HEAD/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /backend/api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | api = Blueprint('api', __name__) 4 | 5 | from .routes import * 6 | -------------------------------------------------------------------------------- /backend/audio_cache/output.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobhaotian/speech-driven-lessons/HEAD/backend/audio_cache/output.mp3 -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation' 2 | 3 | export default function HomePage() { 4 | redirect('/welcome') 5 | } 6 | 7 | -------------------------------------------------------------------------------- /backend/__pycache__/chatbot.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobhaotian/speech-driven-lessons/HEAD/backend/__pycache__/chatbot.cpython-313.pyc -------------------------------------------------------------------------------- /app/my-publish/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | 3 | export default function MyPublishRedirect() { 4 | redirect("/my-publish/my-channel") 5 | } -------------------------------------------------------------------------------- /backend/utils/__pycache__/s3_utils.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobhaotian/speech-driven-lessons/HEAD/backend/utils/__pycache__/s3_utils.cpython-313.pyc -------------------------------------------------------------------------------- /backend/__pycache__/context_manager.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobhaotian/speech-driven-lessons/HEAD/backend/__pycache__/context_manager.cpython-313.pyc -------------------------------------------------------------------------------- /backend/utils/__pycache__/user_utils.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobhaotian/speech-driven-lessons/HEAD/backend/utils/__pycache__/user_utils.cpython-313.pyc -------------------------------------------------------------------------------- /backend/__pycache__/s3_context_manager.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobhaotian/speech-driven-lessons/HEAD/backend/__pycache__/s3_context_manager.cpython-313.pyc -------------------------------------------------------------------------------- /backend/utils/__pycache__/socket_utils.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobhaotian/speech-driven-lessons/HEAD/backend/utils/__pycache__/socket_utils.cpython-313.pyc -------------------------------------------------------------------------------- /backend/utils/__pycache__/course_manager.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobhaotian/speech-driven-lessons/HEAD/backend/utils/__pycache__/course_manager.cpython-313.pyc -------------------------------------------------------------------------------- /backend/utils/__pycache__/firebase_admin.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobhaotian/speech-driven-lessons/HEAD/backend/utils/__pycache__/firebase_admin.cpython-313.pyc -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /backend/functions/__pycache__/slides_navigation.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobhaotian/speech-driven-lessons/HEAD/backend/functions/__pycache__/slides_navigation.cpython-313.pyc -------------------------------------------------------------------------------- /backend/utils/__pycache__/load_and_process_index.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobhaotian/speech-driven-lessons/HEAD/backend/utils/__pycache__/load_and_process_index.cpython-313.pyc -------------------------------------------------------------------------------- /backend/functions/__pycache__/get_detailed_content.cpython-313.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bobhaotian/speech-driven-lessons/HEAD/backend/functions/__pycache__/get_detailed_content.cpython-313.pyc -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /backend/api/user.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | import utils.user_utils as user_utils 3 | 4 | user = Blueprint('user', __name__) 5 | 6 | @user.route('/verify-user', methods=['POST', 'OPTIONS']) 7 | def verify_user(): 8 | """Route handler for verifying user token and initializing S3""" 9 | return user_utils.handle_verify_user() -------------------------------------------------------------------------------- /app/courses/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React from 'react' 4 | import { CourseProvider } from "@/lib/course-context" 5 | 6 | export default function CoursesLayout({ children }: { children: React.ReactNode }) { 7 | return ( 8 | 9 | {children} 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React from 'react' 4 | import { CourseProvider } from "@/lib/course-context" 5 | 6 | export default function DashboardLayout({ children }: { children: React.ReactNode }) { 7 | return ( 8 | 9 | {children} 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /backend/user_files_utils.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import os 3 | 4 | # Set up AWS credentials and S3 resource 5 | ACCESS_KEY = os.getenv("AWS_ACCESS_KEY_ID") 6 | SECRET_KEY = os.getenv("AWS_SECRET_ACCESS_KEY") 7 | REGION_NAME = "ca-central-1" # e.g., 'us-east-1' 8 | 9 | # Initialize S3 client 10 | s3_client = boto3.client( 11 | 's3', 12 | aws_access_key_id=ACCESS_KEY, 13 | aws_secret_access_key=SECRET_KEY, 14 | region_name=REGION_NAME 15 | ) 16 | 17 | bucket_name = "jasmintechs-tutorion" -------------------------------------------------------------------------------- /app/services/chatService.ts: -------------------------------------------------------------------------------- 1 | // app/services/chatService.ts 2 | export const initializeChatbot = async (courseTitle: string) => { 3 | const response = await fetch("http://localhost:5000/api/initialize-chatbot", { 4 | method: "POST", 5 | headers: { "Content-Type": "application/json" }, 6 | body: JSON.stringify({ course_title: courseTitle }), 7 | }); 8 | return response.json(); 9 | }; 10 | 11 | export const getAIResponse = async (input: string) => { 12 | // API call implementation 13 | }; 14 | -------------------------------------------------------------------------------- /backend/functions/get_detailed_content.py: -------------------------------------------------------------------------------- 1 | from s3_context_manager import ContextManager as S3ContextManager 2 | import utils.s3_utils as s3_utils 3 | from dotenv import load_dotenv 4 | import os 5 | 6 | load_dotenv() 7 | 8 | API_KEY = os.getenv("OPENAI_API_KEY") 9 | 10 | s3_bucket = "jasmintechs-tutorion" 11 | 12 | def get_detailed_content(course_title, user, user_query): 13 | s3_context_manager = S3ContextManager(user, course_title, api_key=API_KEY) 14 | s3_context_manager.load_saved_indices() 15 | return s3_context_manager.get_relevant_chunks(user_query) 16 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | 2 | import './globals.css' 3 | import type { Metadata } from 'next' 4 | import { Inter } from 'next/font/google' 5 | 6 | const inter = Inter({ subsets: ['latin'] }) 7 | 8 | export const metadata: Metadata = { 9 | title: 'Student Dashboard', 10 | description: 'A comprehensive learning platform with AI tutors', 11 | } 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode 17 | }) { 18 | return ( 19 | 20 | 21 | {children} 22 | 23 | 24 | ) 25 | } 26 | 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # Testing 7 | /coverage 8 | 9 | # Next.js 10 | /.next/ 11 | /out/ 12 | 13 | # Production 14 | /build 15 | 16 | # Misc 17 | .DS_Store 18 | *.pem 19 | 20 | # Logs 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Local .env files 26 | .env 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # Python 33 | __pycache__/ 34 | *.pyc 35 | *.pyo 36 | *.pyd 37 | .Python 38 | env/ 39 | venv/ 40 | pip-selfcheck.json 41 | *.egg-info/ 42 | dist/ 43 | build/ 44 | 45 | # Backend .env 46 | backend/.env -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "speech-driven-lessons", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "next": "14.2.4", 13 | "react": "^18", 14 | "react-dom": "^18" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^20", 18 | "@types/react": "^18", 19 | "@types/react-dom": "^18", 20 | "postcss": "^8", 21 | "tailwindcss": "^3.4.1", 22 | "typescript": "^5" 23 | } 24 | } -------------------------------------------------------------------------------- /backend/api/routes.py: -------------------------------------------------------------------------------- 1 | from .webhook import webhook 2 | from .assistant import assistant 3 | from .in_class import in_class 4 | from .course import course 5 | from .course_generation import course_generation 6 | from .user import user 7 | from flask import request, jsonify 8 | from . import api 9 | 10 | # Register blueprints 11 | api.register_blueprint(webhook, url_prefix='/webhook') 12 | api.register_blueprint(assistant, url_prefix='/assistant') 13 | api.register_blueprint(in_class, url_prefix='/in-class') 14 | 15 | api.register_blueprint(course, url_prefix='/course') 16 | api.register_blueprint(user, url_prefix='/user') 17 | api.register_blueprint(course_generation, url_prefix='/course_generation') -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /app/welcome/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from 'react'; 4 | import { useRouter } from 'next/navigation'; 5 | import WelcomeAnimation from "@/components/animations/WelcomeAnimation"; 6 | import { useAuth } from "@/auth/firebase"; 7 | 8 | export default function WelcomePage() { 9 | const router = useRouter(); 10 | const { userEmail, loading, error } = useAuth(); 11 | 12 | useEffect(() => { 13 | // Automatically redirect to dashboard after animation completes 14 | const timer = setTimeout(() => { 15 | router.push('/dashboard'); 16 | }, 2600); 17 | 18 | return () => clearTimeout(timer); 19 | }, [router]); 20 | 21 | return ( 22 |
23 | 24 |
25 | ); 26 | } 27 | 28 | 29 | -------------------------------------------------------------------------------- /backend/utils/socket_utils.py: -------------------------------------------------------------------------------- 1 | # Create this new file 2 | 3 | # This module will hold a reference to the socketio instance 4 | # to avoid circular imports 5 | _socketio = None 6 | 7 | def init_socketio(socketio_instance): 8 | """Initialize the socketio reference""" 9 | global _socketio 10 | _socketio = socketio_instance 11 | 12 | def emit_slide_change(assistant_id, position): 13 | """Emit slide change event to a specific room""" 14 | if _socketio: 15 | _socketio.emit('slide_changed', {'position': position}, room=assistant_id) 16 | else: 17 | print("Warning: socketio not initialized yet") 18 | 19 | def emit_assistant_activity(assistant_id): 20 | """Emit assistant activity event to reset inactivity timer""" 21 | if _socketio: 22 | _socketio.emit('assistant_activity', room=assistant_id) 23 | else: 24 | print("Warning: socketio not initialized yet") -------------------------------------------------------------------------------- /backend/routes/delete_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, jsonify 2 | import utils.user_utils as user_utils 3 | import utils.s3_utils as s3_utils 4 | import os 5 | 6 | delete_bp = Blueprint('delete', __name__) 7 | UPLOAD_FOLDER = "../uploads" 8 | 9 | @delete_bp.route('/delete-file', methods=['POST']) 10 | def delete_file(): 11 | username = user_utils.get_current_user(request) 12 | if not username: 13 | return jsonify({'error': 'Unauthorized'}), 401 14 | 15 | data = request.json 16 | filename = data.get('filename').replace(" ", "_") 17 | coursename = data.get('coursename') 18 | 19 | if not filename or not coursename: 20 | return jsonify({'error': 'filename and coursename are required'}), 400 21 | 22 | file_path = s3_utils.get_s3_file_path(username, coursename, filename) 23 | response = s3_utils.delete_file_from_s3("jasmintechs-tutorion", file_path) 24 | 25 | if response: 26 | return jsonify({'message': 'File deleted successfully'}) 27 | else: 28 | return jsonify({'error': 'File not found'}), 404 29 | -------------------------------------------------------------------------------- /backend/routes/delete_course_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, jsonify 2 | import utils.user_utils as user_utils # Assuming this utility gets the current username 3 | import utils.s3_utils as s3_utils # Assuming this utility interacts with S3 4 | 5 | delete_course_bp = Blueprint('delete_course', __name__) 6 | 7 | @delete_course_bp.route('/delete-course', methods=['POST']) 8 | def delete_course(): 9 | username = user_utils.get_current_user(request) 10 | if not username: 11 | return jsonify({'error': 'Unauthorized'}), 401 12 | 13 | data = request.json 14 | course_id = data.get('id') 15 | title = data.get('title') 16 | 17 | print("course_id: ", course_id) 18 | print("title: ", title) 19 | 20 | if not course_id or not title: 21 | return jsonify({'error': 'id and title are required'}), 400 22 | 23 | response = s3_utils.delete_folder_from_s3("jasmintechs-tutorion", s3_utils.get_course_s3_folder(username, title)) 24 | 25 | if response: 26 | return jsonify({'message': 'Course deleted successfully'}) 27 | else: 28 | return jsonify({'error': 'Course not found'}), 404 29 | -------------------------------------------------------------------------------- /backend/routes/upload_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, jsonify 2 | from werkzeug.utils import secure_filename 3 | import utils.user_utils as user_utils 4 | import utils.s3_utils as s3_utils 5 | 6 | upload_bp = Blueprint('upload', __name__) 7 | UPLOAD_FOLDER = "../uploads" 8 | 9 | @upload_bp.route('/upload-files', methods=['POST']) 10 | def upload_files(): 11 | username = user_utils.get_current_user(request) 12 | if not username: 13 | return jsonify({'error': 'Unauthorized'}), 401 14 | 15 | if 'files' not in request.files: 16 | return jsonify({'error': 'No files part'}), 400 17 | 18 | if 'coursename' not in request.form: 19 | return jsonify({'error': 'No course name'}), 400 20 | 21 | coursename = request.form['coursename'] 22 | files = request.files.getlist('files') 23 | uploaded_files = [] 24 | 25 | for file in files: 26 | if file.filename: 27 | filename = secure_filename(file.filename) 28 | file.seek(0) 29 | s3_utils.upload_file_to_s3(file, "jasmintechs-tutorion", 30 | s3_utils.get_s3_file_path(username, coursename, filename)) 31 | uploaded_files.append(filename) 32 | 33 | return jsonify({'message': 'Files uploaded successfully', 'files': uploaded_files}) 34 | -------------------------------------------------------------------------------- /backend/routes/course_info_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, jsonify 2 | import os 3 | import json 4 | import utils.user_utils as user_utils # Assuming this utility gets the current username 5 | import utils.s3_utils as s3_utils # Assuming this utility interacts with S3 6 | 7 | course_info_bp = Blueprint('course_info', __name__) 8 | 9 | @course_info_bp.route('/course_info', methods=['POST']) 10 | def course_info(): 11 | username = user_utils.get_current_user(request) 12 | if not username: 13 | return jsonify({'error': 'Unauthorized'}), 401 14 | 15 | try: 16 | # Get course info from S3 17 | s3_course_info = s3_utils.get_s3_user_courses_info(username) 18 | 19 | # Ensure we're returning a list of courses 20 | if not isinstance(s3_course_info, list): 21 | s3_course_info = [] 22 | 23 | # Process each course to ensure required fields exist 24 | processed_courses = [] 25 | for course in s3_course_info: 26 | processed_course = { 27 | 'id': course.get('id'), 28 | 'title': course.get('title', 'Untitled Course'), 29 | 'progress': course.get('progress', 0), 30 | 'hoursCompleted': course.get('hoursCompleted', 0), 31 | 'author': course.get('author', 'Unknown Instructor') 32 | } 33 | processed_courses.append(processed_course) 34 | 35 | return jsonify({'courses': processed_courses}) 36 | 37 | except Exception as e: 38 | return jsonify({'error': str(e)}), 500 39 | -------------------------------------------------------------------------------- /app/courses/[id]/types.ts: -------------------------------------------------------------------------------- 1 | // app/courses/[id]/types.ts 2 | export interface Message { 3 | id: number 4 | sender: "user" | "ai" 5 | text: string 6 | timestamp: Date 7 | slides?: { title: string; content: string }[] 8 | } 9 | 10 | export interface File { 11 | id: string 12 | name: string 13 | size: string 14 | type: string 15 | uploadedAt: string 16 | } 17 | 18 | declare global { 19 | interface Window { 20 | SpeechRecognition: { 21 | new(): SpeechRecognition 22 | } 23 | webkitSpeechRecognition: { 24 | new(): SpeechRecognition 25 | } 26 | } 27 | } 28 | 29 | export interface SpeechRecognition extends EventTarget { 30 | continuous: boolean 31 | interimResults: boolean 32 | lang: string 33 | maxAlternatives: number 34 | onend: ((this: SpeechRecognition, ev: Event) => any) | null 35 | onerror: ((this: SpeechRecognition, ev: Event) => any) | null 36 | onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => any) | null 37 | onstart: ((this: SpeechRecognition, ev: Event) => any) | null 38 | start: () => void 39 | stop: () => void 40 | abort: () => void 41 | } 42 | 43 | export interface SpeechRecognitionEvent extends Event { 44 | results: SpeechRecognitionResultList 45 | resultIndex: number 46 | } 47 | 48 | export interface SpeechRecognitionResultList { 49 | length: number 50 | item(index: number): SpeechRecognitionResult 51 | [index: number]: SpeechRecognitionResult 52 | } 53 | 54 | export interface SpeechRecognitionResult { 55 | isFinal: boolean 56 | length: number 57 | item(index: number): SpeechRecognitionAlternative 58 | [index: number]: SpeechRecognitionAlternative 59 | } 60 | 61 | export interface SpeechRecognitionAlternative { 62 | confidence: number 63 | transcript: string 64 | } -------------------------------------------------------------------------------- /app/courses/[id]/components/SlideViewer.tsx: -------------------------------------------------------------------------------- 1 | // app/courses/[id]/components/SlideViewer.tsx 2 | 'use client' 3 | 4 | import { Button } from "@/components/ui/button" 5 | import { Slide } from "@/components/slide" 6 | 7 | export const SlideViewer = ({ 8 | slides, 9 | currentSlideIndex, 10 | setCurrentSlideIndex 11 | }: { 12 | slides: { title: string; content: string }[], 13 | currentSlideIndex: number, 14 | setCurrentSlideIndex: (index: number) => void 15 | }) => { 16 | return ( 17 |
18 |
19 | {slides.map((slide, index) => ( 20 | 27 | ))} 28 |
29 | 30 |
31 | 38 | 39 | Slide {currentSlideIndex + 1} of {slides.length} 40 | 41 | 48 |
49 |
50 | ) 51 | } -------------------------------------------------------------------------------- /app/courses/[id]/components/ChatHistory.tsx: -------------------------------------------------------------------------------- 1 | // app/courses/[id]/components/ChatHistory.tsx 2 | 'use client' 3 | 4 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" 5 | import { ScrollArea } from "@/components/ui/scroll-area" 6 | import { Bot } from 'lucide-react' 7 | import { Message } from "../types" 8 | 9 | export const ChatHistory = ({ messages, isTyping }: { messages: Message[], isTyping: boolean }) => { 10 | return ( 11 | 12 |
13 | { 14 | messages.map(message => ( 15 |
16 | { 17 | message.sender === "ai" ? ( 18 | 19 | 20 | 21 | 22 | ) : ( 23 | 24 | 25 | U 26 | 27 | )} 28 |
29 |

{ message.text }

30 | < span className = "text-xs text-gray-500 dark:text-gray-400 mt-1 block" > 31 | { message.timestamp.toLocaleTimeString() } 32 | 33 |
34 |
35 | ))} 36 | { 37 | isTyping && ( 38 |
39 | 40 | 41 | 42 | 43 | < div className = "flex-1 bg-blue-100 dark:bg-blue-900 p-3 rounded-lg" > 44 |

Typing...

45 |
46 |
47 | ) 48 | } 49 | 50 |
51 | ) 52 | } -------------------------------------------------------------------------------- /app/courses/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { MainNav } from "@/components/main-nav" 4 | import { CourseCard } from "@/components/course-card" 5 | import { CreateCourseModal } from "@/components/create-course-modal" 6 | import { Button } from "@/components/ui/button" 7 | import { Plus } from 'lucide-react' 8 | import { useState } from "react" 9 | import { useCourses } from "@/lib/course-context" 10 | 11 | export default function CoursesPage() { 12 | const [createModalOpen, setCreateModalOpen] = useState(false) 13 | const [courseToEdit, setCourseToEdit] = useState(null); 14 | const { courses, removeCourse, addCourse } = useCourses() 15 | 16 | const handleCustomize = (course) => { 17 | setCourseToEdit(course); // 设置要编辑的课程 18 | setCreateModalOpen(true); // 打开 modal 19 | }; 20 | 21 | const handleAddCourse = () => { 22 | setCourseToEdit(null); // 清空编辑课程 23 | setCreateModalOpen(true); // 打开 modal 24 | }; 25 | 26 | return ( 27 |
28 | 29 |
30 |
31 |

My Courses

32 | 36 |
37 |
38 | {courses.map((course) => ( 39 | handleCustomize(course)} // 传递当前课程 48 | onRemove={() => removeCourse(course.id, course.title)} 49 | /> 50 | ))} 51 |
52 | { 56 | addCourse(courseData); 57 | setCreateModalOpen(false); 58 | }} 59 | courseToEdit={courseToEdit} // 传递要编辑的课程 60 | /> 61 |
62 |
63 | ); 64 | } 65 | 66 | -------------------------------------------------------------------------------- /backend/routes/voice_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, jsonify, send_file 2 | import edge_tts 3 | import asyncio 4 | import os 5 | import tempfile 6 | import openai 7 | voice_bp = Blueprint('voice', __name__) 8 | AUDIO_DIR = "audio_cache" 9 | os.makedirs(AUDIO_DIR, exist_ok=True) 10 | 11 | client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY")) 12 | @voice_bp.route('/api/list-voices', methods=['GET']) 13 | def list_voices(): 14 | try: 15 | voices = asyncio.run(edge_tts.list_voices()) 16 | return jsonify(voices) 17 | except Exception as e: 18 | return jsonify({"error": str(e)}), 500 19 | 20 | @voice_bp.route('/api/generate-audio', methods=['POST']) 21 | def generate_audio(): # Remove async 22 | data = request.json 23 | text = data.get("text", "") 24 | voice = data.get("voice", "en-US-AvaMultilingualNeural") 25 | 26 | if not text: 27 | return jsonify({"error": "No text provided"}), 400 28 | 29 | # File path 30 | audio_file = os.path.join(AUDIO_DIR, "output.mp3") 31 | try: 32 | # Wrap the async operations in asyncio.run() 33 | async def generate(): 34 | communicate = edge_tts.Communicate(text, voice) 35 | await communicate.save(audio_file) 36 | 37 | asyncio.run(generate()) 38 | return send_file(audio_file, as_attachment=True) 39 | except Exception as e: 40 | return jsonify({"error": str(e)}), 500 41 | 42 | 43 | @voice_bp.route('/api/recognize-openai', methods=['POST']) 44 | def recognize_with_openai(): 45 | if 'audio' not in request.files: 46 | return jsonify({"error": "No audio file uploaded"}), 400 47 | 48 | audio_file = request.files['audio'] 49 | 50 | try: 51 | # Save the audio file to a temporary location 52 | with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file: 53 | audio_path = temp_file.name 54 | audio_file.save(audio_path) 55 | 56 | # Use OpenAI Whisper API 57 | with open(audio_path, "rb") as file: 58 | response = client.audio.transcriptions.create( 59 | model="whisper-1", 60 | file=file, 61 | response_format="verbose_json", 62 | timestamp_granularities=["segment", "word"] 63 | ) 64 | 65 | print(f"response: {response}") 66 | 67 | # Clean up the temporary file 68 | os.remove(audio_path) 69 | 70 | print(f"user prompt: {response.text}") 71 | 72 | # Return the transcription text 73 | return jsonify({"text": response.text}) 74 | except Exception as e: 75 | print(f"Error in recognize_with_openai: {str(e)}") 76 | return jsonify({"error": str(e)}), 500 -------------------------------------------------------------------------------- /backend/api/in_class.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from flask import Blueprint, request, jsonify 4 | import utils.user_utils as user_utils 5 | import utils.s3_utils as s3_utils 6 | from dotenv import load_dotenv 7 | from functions.slides_navigation import get_slides, get_current_slide, set_current_slide 8 | from utils.socket_utils import emit_slide_change 9 | import datetime 10 | 11 | load_dotenv() 12 | 13 | S3_BUCKET_NAME = "jasmintechs-tutorion" 14 | 15 | in_class = Blueprint('in-class', __name__) 16 | 17 | @in_class.route('/next-slide', methods=['POST']) 18 | def next_slide(): 19 | username = user_utils.get_current_user(request) 20 | if not username: 21 | return jsonify({'error': 'Unauthorized'}), 401 22 | 23 | request_data = request.get_json() 24 | assistant_id = request_data.get('assistant_id', '') 25 | 26 | user_course_data = s3_utils.load_assistant_user_from_s3(assistant_id) 27 | course_id = user_course_data['course_id'] 28 | 29 | if not assistant_id: 30 | return jsonify({'error': 'Assistant ID is required'}), 400 31 | 32 | print(f"Going to next slide for {assistant_id}") 33 | slides = get_slides(course_id, username) 34 | if not slides: 35 | return "No slides found for this course." 36 | current_position = get_current_slide(assistant_id) 37 | 38 | new_position = current_position + 1 39 | if new_position < len(slides): 40 | set_current_slide(assistant_id, new_position) 41 | # Emit event to frontend via Socket.IO 42 | emit_slide_change(assistant_id, new_position) 43 | return "Here is the transcript you would like to read for the next slide: " + slides[new_position]['transcript'] 44 | else: 45 | return "You're already at the last slide." 46 | 47 | @in_class.route('/save-position', methods=['POST']) 48 | def save_assistant_position(): 49 | username = user_utils.get_current_user(request) 50 | if not username: 51 | return jsonify({'error': 'Unauthorized'}), 401 52 | 53 | request_data = request.get_json() 54 | assistant_id = request_data.get('assistant_id', '') 55 | course_id = request_data.get('course_id', '') 56 | position = request_data.get('position') 57 | 58 | if not assistant_id or position is None or not course_id: 59 | return jsonify({'error': 'Missing required parameters'}), 400 60 | 61 | try: 62 | # First, get user data for this assistant 63 | user_data = s3_utils.load_assistant_user_from_s3(assistant_id) 64 | if not user_data: 65 | return jsonify({'error': 'Assistant not found'}), 404 66 | 67 | # Save the position to S3 68 | s3_path = s3_utils.get_s3_file_path(username, course_id, "assistant_position.json") 69 | position_data = { 70 | "assistant_id": assistant_id, 71 | "course_id": course_id, 72 | "last_position": position, 73 | "timestamp": str(datetime.datetime.now()) 74 | } 75 | 76 | s3_utils.upload_json_to_s3(position_data, s3_utils.S3_BUCKET_NAME, s3_path) 77 | 78 | return jsonify({'success': True, 'message': 'Position saved successfully'}), 200 79 | except Exception as e: 80 | print(f"Error saving assistant position: {e}") 81 | return jsonify({'error': f'Failed to save position: {str(e)}'}), 500 82 | 83 | 84 | -------------------------------------------------------------------------------- /backend/utils/firebase_admin.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import firebase_admin 4 | from firebase_admin import credentials, auth 5 | from dotenv import load_dotenv 6 | 7 | # Configure logging 8 | logging.basicConfig(level=logging.INFO) 9 | logger = logging.getLogger(__name__) 10 | 11 | # Load environment variables 12 | load_dotenv() 13 | 14 | def initialize_firebase_admin(): 15 | """Initialize Firebase Admin SDK with credentials from environment variables""" 16 | try: 17 | # Get the private key and ensure it's properly formatted 18 | private_key = os.getenv("FIREBASE_PRIVATE_KEY", "").replace("\\n", "\n") 19 | if not private_key: 20 | raise ValueError("FIREBASE_PRIVATE_KEY environment variable is not set") 21 | 22 | # Create the service account info dictionary 23 | service_account_info = { 24 | "type": "service_account", 25 | "project_id": os.getenv("FIREBASE_PROJECT_ID"), 26 | "private_key_id": os.getenv("FIREBASE_PRIVATE_KEY_ID"), 27 | "private_key": private_key, 28 | "client_email": os.getenv("FIREBASE_CLIENT_EMAIL"), 29 | "client_id": os.getenv("FIREBASE_CLIENT_ID"), 30 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 31 | "token_uri": "https://oauth2.googleapis.com/token", 32 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 33 | "client_x509_cert_url": os.getenv("FIREBASE_CLIENT_CERT_URL") 34 | } 35 | 36 | # Verify all required fields are present 37 | required_fields = ["project_id", "private_key_id", "private_key", "client_email", "client_id", "client_x509_cert_url"] 38 | missing_fields = [field for field in required_fields if not service_account_info.get(field)] 39 | if missing_fields: 40 | raise ValueError(f"Missing required Firebase configuration fields: {', '.join(missing_fields)}") 41 | 42 | # Initialize Firebase Admin if not already initialized 43 | if not firebase_admin._apps: 44 | cred = credentials.Certificate(service_account_info) 45 | firebase_admin.initialize_app(cred) 46 | logger.info("Firebase Admin SDK initialized successfully") 47 | else: 48 | logger.info("Firebase Admin SDK already initialized") 49 | 50 | return True 51 | except Exception as e: 52 | logger.error(f"Error initializing Firebase Admin SDK: {e}") 53 | return False 54 | 55 | def verify_firebase_token(token): 56 | """Verify Firebase ID token and return decoded token or None if invalid""" 57 | try: 58 | if not token: 59 | logger.warning("No token provided") 60 | return None 61 | 62 | # Remove 'Bearer ' prefix if present 63 | if token.startswith('Bearer '): 64 | token = token[7:] 65 | 66 | try: 67 | # Verify the token 68 | decoded_token = auth.verify_id_token(token) 69 | logger.info(f"Token verified successfully for user: {decoded_token.get('email')}") 70 | return decoded_token 71 | except Exception as e: 72 | logger.error(f"Error verifying token: {str(e)}") 73 | return None 74 | except Exception as e: 75 | logger.error(f"Error in verify_firebase_token: {str(e)}") 76 | return None 77 | 78 | # Initialize Firebase Admin when the module is imported 79 | initialize_firebase_admin() 80 | -------------------------------------------------------------------------------- /backend/routes/aiTutor_routes.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify, Response, send_file, Blueprint 2 | import openai 3 | import os 4 | from langgraph.checkpoint.memory import MemorySaver 5 | from langgraph.graph import START, MessagesState, StateGraph 6 | from langchain_core.messages import HumanMessage, AIMessage 7 | 8 | 9 | aitutor_bp = Blueprint('aitutor', __name__) 10 | 11 | # Initialize OpenAI client 12 | client = openai.OpenAI(api_key=os.getenv("OPENAI_API_KEY")) 13 | 14 | # Define a new LangChain graph 15 | workflow = StateGraph(state_schema=MessagesState) 16 | 17 | # Define the function that calls the OpenAI model 18 | def call_model(state: MessagesState): 19 | try: 20 | # Prepare system message and state messages for the conversation 21 | system_message = { 22 | "role": "system", 23 | "content": '''You are a helpful AI assistant for the AI Tutor platform 'Tutorion'. Students will ask you some questions after or before their online courses. 24 | Make sure your response is concise and to the point, don't be too formal and don't be too long. 25 | --- 26 | Some of our information: your role is to help students with their online courses. Go to courses tab if they want to add their customized courses, modify their courses or go and start their classes. 27 | Go to schedule tab if they want to see their schedule. 28 | Go to profile tab if they want to see their profile. 29 | Go to progress tab if they want to see their customized courses related data. 30 | ''' 31 | } 32 | 33 | # Add system message at the beginning of the conversation 34 | messages = [system_message] + [{"role": "user", "content": msg.content} for msg in state["messages"]] 35 | 36 | # Call OpenAI model 37 | response = client.chat.completions.create( 38 | model="gpt-4o", 39 | messages=messages, 40 | max_tokens=400, 41 | temperature=0.9 42 | ) 43 | 44 | # Extract AI's response 45 | ai_content = response.choices[0].message.content 46 | 47 | # Return the messages including the AI response 48 | return {"messages": [AIMessage(ai_content)]} 49 | except Exception as e: 50 | print(f"Error in call_model: {str(e)}") 51 | return {"messages": [AIMessage("An error occurred while processing your request.")]} 52 | 53 | # Define the (single) node in the graph 54 | workflow.add_edge(START, "model") 55 | workflow.add_node("model", call_model) 56 | 57 | # Add memory 58 | memory = MemorySaver() 59 | app_workflow = workflow.compile(checkpointer=memory) 60 | 61 | # API Endpoint for handling user input 62 | @aitutor_bp.route('/api/aitutor-response', methods=['POST']) 63 | def get_ai_response(): 64 | try: 65 | data = request.get_json() 66 | if not data: 67 | return {'error': 'No data provided'}, 400 68 | 69 | user_input = data.get('input') 70 | thread_id = data.get('thread_id', 'default_thread') # Support multiple threads by using a thread ID 71 | if not user_input: 72 | return {'error': 'No input provided'}, 400 73 | 74 | # Create HumanMessage for user input 75 | input_messages = [HumanMessage(user_input)] 76 | config = {"configurable": {"thread_id": thread_id}} 77 | 78 | # Invoke the LangChain workflow 79 | output = app_workflow.invoke({"messages": input_messages}, config) 80 | 81 | # Get the AI's latest response 82 | ai_response = output["messages"][-1].content 83 | 84 | return ai_response 85 | #return {'response': ai_response} 86 | except Exception as e: 87 | print(f"Error in get_ai_response: {str(e)}") 88 | return {'error': str(e)}, 500 -------------------------------------------------------------------------------- /app/my-publish/data-center/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { HeaderOnlyLayout } from "@/components/layout/HeaderOnlyLayout" 4 | import { ScrollArea } from "@/components/ui/scroll-area" 5 | import { PublishSidebar } from "@/components/my-publish/publish-page-sidebar" 6 | import { BarChart, LineChart, PieChart } from "lucide-react" 7 | 8 | export default function DataCenterPage() { 9 | return ( 10 | 11 |
12 | {/* Left Sidebar */} 13 | 14 | 15 | {/* Main Content Area */} 16 |
17 | 18 |
19 | {/* Page Header */} 20 |
21 |

Data Center

22 |

Analytics and insights for your content

23 |
24 | 25 | {/* Dashboard Cards */} 26 |
27 |
28 |
29 |

Total Views

30 | 31 |
32 |

6,620

33 |

↑ 12% from last month

34 |
35 | 36 |
37 |
38 |

Watch Time

39 | 40 |
41 |

248 hrs

42 |

↑ 8% from last month

43 |
44 | 45 |
46 |
47 |

Audience

48 | 49 |
50 |

824

51 |

↑ 15% from last month

52 |
53 |
54 | 55 | {/* Charts Placeholder */} 56 |
57 |
58 |

Views Over Time

59 |
60 |

Chart visualization would go here

61 |
62 |
63 | 64 |
65 |

Popular Content

66 |
67 |

Chart visualization would go here

68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | ) 77 | } -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | // import { NextResponse } from 'next/server'; 2 | // import type { NextRequest } from 'next/server'; 3 | // 4 | // // Predefined user credentials 5 | // const USERS = { 6 | // user1: 'password1', 7 | // user2: 'password2', 8 | // }; 9 | // 10 | // export function middleware(request: NextRequest) { 11 | // const authHeader = request.headers.get('authorization'); 12 | // 13 | // // If no authorization header is provided, trigger the browser's login popup 14 | // if (!authHeader) { 15 | // return new Response('Unauthorized', { 16 | // status: 401, 17 | // headers: { 'WWW-Authenticate': 'Basic realm="Login Required"' }, 18 | // }); 19 | // } 20 | // 21 | // // Decode the Authorization header 22 | // const credentials = Buffer.from(authHeader.split(' ')[1], 'base64').toString(); 23 | // const [username, password] = credentials.split(':'); 24 | // 25 | // // Validate the username and password 26 | // if (!(username in USERS && USERS[username] === password)) { 27 | // return new Response('Unauthorized', { 28 | // status: 401, 29 | // headers: { 'WWW-Authenticate': 'Basic realm="Login Required"' }, 30 | // }); 31 | // } 32 | // 33 | // // Pass the authenticated username to the next step 34 | // request.headers.set('x-authenticated-user', username); 35 | // 36 | // // Allow the request to continue to the app 37 | // return NextResponse.next(); 38 | // } 39 | 40 | import { NextResponse } from 'next/server'; 41 | import type { NextRequest } from 'next/server'; 42 | import { parse } from 'cookie'; 43 | 44 | // Predefined user credentials 45 | const USERS = { 46 | user1: 'password1', 47 | user2: 'password2', 48 | }; 49 | 50 | function isAuthenticated(request: NextRequest) { 51 | const cookieHeader = request.headers.get('cookie'); 52 | console.log('Cookie Header:', cookieHeader); 53 | 54 | if (!cookieHeader) return false; 55 | 56 | const cookies = parse(cookieHeader); 57 | console.log('Parsed Cookies:', cookies); 58 | 59 | // Check for auth cookie (predefined user credentials) 60 | const authCookie = cookies.auth; 61 | if (authCookie) { 62 | const credentials = Buffer.from(authCookie, 'base64').toString(); 63 | const [username, password] = credentials.split(':'); 64 | if (username in USERS && (USERS as {[key: string]: string})[username] === password) { 65 | return true; 66 | } 67 | } 68 | 69 | // Check for email cookie (Google authentication) 70 | const emailCookie = cookies.user_email; 71 | if (emailCookie) { 72 | console.log('Authenticated via Google with email:', emailCookie); 73 | return true; 74 | } 75 | 76 | console.log('No valid authentication found'); 77 | return false; 78 | } 79 | 80 | // Middleware function 81 | export function middleware(request: NextRequest) { 82 | const authenticated = isAuthenticated(request); 83 | 84 | console.log('authenticated', authenticated); 85 | console.log('request.nextUrl.pathname', request.nextUrl.pathname); 86 | 87 | const pathname = request.nextUrl.pathname; 88 | 89 | // Exclude requests for static files and assets 90 | if (pathname.startsWith('/_next') || pathname.startsWith('/static')) { 91 | return NextResponse.next(); 92 | } 93 | 94 | if (!authenticated && !request.nextUrl.pathname.startsWith('/login')) { 95 | console.log('redirecting to /login'); 96 | return NextResponse.redirect(new URL('/login', request.url)); 97 | } 98 | 99 | if (authenticated && request.nextUrl.pathname === '/login') { 100 | return NextResponse.redirect(new URL('/dashboard', request.url)); 101 | } 102 | 103 | // Add authenticated username to headers 104 | const response = NextResponse.next(); 105 | const authHeader = request.headers.get('authorization'); 106 | if (authHeader) { 107 | const credentials = Buffer.from(authHeader.split(' ')[1], 'base64').toString(); 108 | const [username] = credentials.split(':'); 109 | response.headers.set('x-authenticated-user', username); 110 | } 111 | 112 | return response; 113 | } 114 | 115 | 116 | -------------------------------------------------------------------------------- /app/my-uploads/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect } from "react" 4 | import { MainLayout } from "@/components/layout/MainLayout" 5 | import { ScrollArea } from "@/components/ui/scroll-area" 6 | import { CustomizeCourseModal } from "@/components/my-uploads/create-course-modal-v2" 7 | import { MyCoursesHeader } from "@/components/my-uploads/my-courses-section-header" 8 | import { CourseCard } from "@/components/my-uploads/CourseCard" 9 | import { FullscreenButton } from "@/components/layout/fullscreen-button" 10 | 11 | // Sample courses data without course codes 12 | // TODO: Replace with API call to fetch courses 13 | // This is temporary mock data that will be replaced with actual data from the backend 14 | const courses = [ 15 | { 16 | id: 1, 17 | title: "Introduction to AI", 18 | hoursCompleted: 7.5, 19 | enrolled: 485, 20 | views: 1250, 21 | isPublished: true 22 | }, 23 | { 24 | id: 2, 25 | title: "Machine Learning Fundamentals", 26 | hoursCompleted: 4.5, 27 | enrolled: 320, 28 | views: 890, 29 | isPublished: true 30 | }, 31 | { 32 | id: 3, 33 | title: "Deep Learning with Python", 34 | hoursCompleted: 2, 35 | enrolled: 156, 36 | views: 430, 37 | isPublished: false 38 | }, 39 | { 40 | id: 4, 41 | title: "Natural Language Processing and Contextual Understanding in Modern Applications", 42 | hoursCompleted: 6, 43 | enrolled: 278, 44 | views: 615, 45 | isPublished: true 46 | }, 47 | { 48 | id: 5, 49 | title: "CV", // Very short title to test that case 50 | hoursCompleted: 3, 51 | enrolled: 92, 52 | views: 205, 53 | isPublished: false 54 | }, 55 | { 56 | id: 6, 57 | title: "Frontend Development with React and TypeScript", 58 | hoursCompleted: 5, 59 | enrolled: 347, 60 | views: 780, 61 | isPublished: false 62 | } 63 | ] 64 | 65 | export default function CoursesPage() { 66 | const [isFullScreen, setIsFullScreen] = useState(false) 67 | // TODO: Add state for courses 68 | // const [courses, setCourses] = useState([]) 69 | // const [isLoading, setIsLoading] = useState(true) 70 | // const [error, setError] = useState(null) 71 | 72 | // TODO: API endpoint - Fetch courses from backend 73 | // useEffect(() => { 74 | // async function fetchCourses() { 75 | // try { 76 | // setIsLoading(true); 77 | // // const response = await fetch('/api/courses'); 78 | // // if (!response.ok) throw new Error('Failed to fetch courses'); 79 | // // const data = await response.json(); 80 | // // setCourses(data); 81 | // } catch (error) { 82 | // // setError(error.message); 83 | // console.error('Error fetching courses:', error); 84 | // } finally { 85 | // // setIsLoading(false); 86 | // } 87 | // } 88 | // fetchCourses(); 89 | // }, []); 90 | 91 | // Function to toggle fullscreen mode 92 | const toggleFullScreen = () => { 93 | if (!document.fullscreenElement) { 94 | document.documentElement.requestFullscreen().catch(err => { 95 | console.log(`Error attempting to enable fullscreen: ${err.message}`); 96 | }); 97 | setIsFullScreen(true); 98 | } else { 99 | if (document.exitFullscreen) { 100 | document.exitFullscreen(); 101 | setIsFullScreen(false); 102 | } 103 | } 104 | }; 105 | 106 | // Listen for fullscreen change events 107 | useEffect(() => { 108 | const handleFullscreenChange = () => { 109 | setIsFullScreen(!!document.fullscreenElement); 110 | }; 111 | 112 | document.addEventListener('fullscreenchange', handleFullscreenChange); 113 | return () => { 114 | document.removeEventListener('fullscreenchange', handleFullscreenChange); 115 | }; 116 | }, []); 117 | 118 | return ( 119 | 120 |
121 | 122 |
123 |
124 |
125 | 129 |
130 |
131 | 135 |
136 |
137 | 138 |
139 | 140 | {courses.map((course) => ( 141 | 142 | ))} 143 |
144 |
145 |
146 |
147 |
148 | ) 149 | } 150 | 151 | -------------------------------------------------------------------------------- /app/course-preview/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useEffect } from "react" 4 | import { useRouter } from "next/navigation" 5 | import { Button } from "@/components/ui/button" 6 | import { ArrowLeft, Maximize, Minimize } from "lucide-react" 7 | import { LightCourseLayout } from "@/components/layout/LightCourseLayout" 8 | import { SlideViewer } from "@/components/creator-edit/slide-viewer" 9 | 10 | export default function CoursePreviewPage({ params }: { params: { id: string } }) { 11 | const courseId = params.id 12 | const router = useRouter() 13 | const [currentSlide, setCurrentSlide] = useState(0) 14 | const [slides, setSlides] = useState([]) 15 | const [isFullScreen, setIsFullScreen] = useState(false) 16 | const [courseTitle, setCourseTitle] = useState("Course Preview") 17 | 18 | // Load course data from localStorage on mount 19 | useEffect(() => { 20 | const savedData = localStorage.getItem('previewCourseData') 21 | if (savedData) { 22 | const data = JSON.parse(savedData) 23 | setSlides(data.slides || []) 24 | setCurrentSlide(data.currentSlide || 0) 25 | setCourseTitle(`Preview: ${data.slides[data.currentSlide]?.title || 'Course'}`) 26 | } else { 27 | // Redirect back if no data is found 28 | router.push(`/creator-edit/${courseId}`) 29 | } 30 | }, [courseId, router]) 31 | 32 | // Function to toggle fullscreen mode 33 | const toggleFullScreen = () => { 34 | if (!document.fullscreenElement) { 35 | document.documentElement.requestFullscreen().catch(err => { 36 | console.log(`Error attempting to enable fullscreen: ${err.message}`) 37 | }) 38 | setIsFullScreen(true) 39 | } else { 40 | document.exitFullscreen() 41 | setIsFullScreen(false) 42 | } 43 | } 44 | 45 | // Add fullscreen listener 46 | useEffect(() => { 47 | const handleFullscreenChange = () => { 48 | setIsFullScreen(!!document.fullscreenElement) 49 | } 50 | 51 | document.addEventListener('fullscreenchange', handleFullscreenChange) 52 | return () => { 53 | document.removeEventListener('fullscreenchange', handleFullscreenChange) 54 | } 55 | }, []) 56 | 57 | // Navigation functions 58 | const goToPrevSlide = () => { 59 | if (currentSlide > 0) { 60 | setCurrentSlide(currentSlide - 1) 61 | setCourseTitle(`Preview: ${slides[currentSlide - 1]?.title || 'Course'}`) 62 | } 63 | } 64 | 65 | const goToNextSlide = () => { 66 | if (currentSlide < slides.length - 1) { 67 | setCurrentSlide(currentSlide + 1) 68 | setCourseTitle(`Preview: ${slides[currentSlide + 1]?.title || 'Course'}`) 69 | } 70 | } 71 | 72 | return ( 73 | 74 | {/* Control buttons */} 75 |
76 | 83 |
84 | 85 | 96 | 97 | {/* Main content */} 98 |
99 |
100 | {/* Slide viewer */} 101 | {slides.length > 0 && ( 102 | 107 | )} 108 | 109 | {/* Navigation controls */} 110 |
111 | 118 |
119 | Slide {currentSlide + 1} of {slides.length} 120 |
121 | 128 |
129 |
130 |
131 |
132 | ) 133 | } -------------------------------------------------------------------------------- /app/courses/[id]/hooks/useConversation.ts: -------------------------------------------------------------------------------- 1 | // app/courses/[id]/hooks/useConversation.ts 2 | 'use client' 3 | 4 | import { useState, useEffect, useRef } from "react" 5 | import { Message } from "../types" 6 | import { useRouter } from 'next/navigation' 7 | import { useSpeech } from './useSpeech' 8 | 9 | export const useConversation = (courseId: string) => { 10 | const { speakResponse } = useSpeech(async () => null) 11 | const router = useRouter() 12 | 13 | const [messages, setMessages] = useState([ 14 | { 15 | id: 1, 16 | sender: "ai", 17 | text: "Hello! I'm your AI tutor for this course. What would you like to learn today?", 18 | timestamp: new Date(), 19 | }, 20 | ]) 21 | const [isTyping, setIsTyping] = useState(false) 22 | const [currentSlideIndex, setCurrentSlideIndex] = useState(0) 23 | 24 | const debounceTimeoutRef = useRef(null) 25 | const lastMessageRef = useRef("") 26 | 27 | useEffect(() => { 28 | const initializeChatbot = async () => { 29 | try { 30 | const decodedTitle = decodeURIComponent(courseId) 31 | const response = await fetch("http://localhost:5000/api/initialize-chatbot", { 32 | method: "POST", 33 | headers: { "Content-Type": "application/json" }, 34 | credentials: "include", 35 | body: JSON.stringify({ course_title: decodedTitle }), 36 | }) 37 | 38 | if (!response.ok) throw new Error('Failed to initialize chatbot') 39 | 40 | const data = await response.json() 41 | console.log("Chatbot initialized:", data) 42 | } catch (error) { 43 | console.error("Error initializing chatbot:", error) 44 | } 45 | } 46 | 47 | initializeChatbot() 48 | }, [courseId]) 49 | 50 | const handleConversationCycle = async (userInput: string): Promise => { 51 | if (!userInput.trim()) return null 52 | 53 | setMessages(prev => [...prev, { 54 | id: Date.now(), 55 | sender: "user", 56 | text: userInput, 57 | timestamp: new Date() 58 | }]) 59 | setIsTyping(true) 60 | 61 | let aiMessageId = Date.now() 62 | let accumulatedText = "" 63 | let lastSpokenLength = 0 64 | 65 | setMessages(prev => [...prev, { 66 | id: aiMessageId, 67 | sender: "ai", 68 | text: "", 69 | timestamp: new Date(), 70 | slides: [] 71 | }]) 72 | 73 | try { 74 | const response = await fetch("http://localhost:5000/api/get-ai-response", { 75 | method: "POST", 76 | headers: { "Content-Type": "application/json" }, 77 | credentials: "include", 78 | body: JSON.stringify({ input: userInput }) 79 | }) 80 | 81 | if (!response.body) throw new Error("No response body received") 82 | 83 | const reader = response.body.getReader() 84 | const decoder = new TextDecoder() 85 | 86 | while (true) { 87 | const { done, value } = await reader.read() 88 | if (done) break 89 | 90 | const chunk = decoder.decode(value, { stream: true }) 91 | const parsedChunks = chunk.trim().split("\n").map(line => JSON.parse(line)) 92 | 93 | for (const { text } of parsedChunks) { 94 | if (text) { 95 | accumulatedText += text 96 | setMessages(prev => 97 | prev.map(msg => msg.id === aiMessageId ? { ...msg, text: accumulatedText } : msg) 98 | ) 99 | 100 | // Speak new content when we have a complete sentence or significant new content 101 | const newContent = accumulatedText.slice(lastSpokenLength) 102 | if (newContent.match(/[.!?]+/) || newContent.length > 100) { 103 | await speakResponse(newContent, true) 104 | lastSpokenLength = accumulatedText.length 105 | } 106 | } 107 | } 108 | } 109 | 110 | // Handle any remaining text that hasn't been spoken 111 | const remainingText = accumulatedText.slice(lastSpokenLength) 112 | if (remainingText.trim()) { 113 | await speakResponse(remainingText, true) 114 | } 115 | 116 | // After streaming is done, request slides 117 | const slidesResponse = await fetch("http://localhost:5000/api/get-slides", { 118 | method: "POST", 119 | headers: { "Content-Type": "application/json" }, 120 | credentials: "include", 121 | body: JSON.stringify({ input: userInput }) 122 | }) 123 | 124 | const slidesData = await slidesResponse.json() 125 | 126 | // Update the AI message with slides if available 127 | setMessages(prev => 128 | prev.map(msg => msg.id === aiMessageId ? { ...msg, slides: slidesData.slides } : msg) 129 | ) 130 | 131 | if (slidesData.slides?.length) setCurrentSlideIndex(0) 132 | 133 | return accumulatedText || "" // Ensure it never returns undefined 134 | } catch (error) { 135 | console.error("Error:", error) 136 | setMessages(prev => prev.map(msg => 137 | msg.id === aiMessageId ? { ...msg, text: "Sorry, I encountered an error. Please try again." } : msg 138 | )) 139 | return null 140 | } finally { 141 | setIsTyping(false) 142 | } 143 | } 144 | 145 | 146 | return { 147 | messages, 148 | isTyping, 149 | handleConversationCycle, 150 | currentSlideIndex, 151 | setCurrentSlideIndex 152 | }; 153 | 154 | } -------------------------------------------------------------------------------- /backend/functions/slides_navigation.py: -------------------------------------------------------------------------------- 1 | import json 2 | import redis 3 | from utils.socket_utils import emit_slide_change 4 | import utils.s3_utils as s3_utils 5 | 6 | # Initialize Redis connection 7 | redis_client = redis.Redis(host='localhost', port=6379, db=0) 8 | 9 | 10 | def get_slides(course_id, username): 11 | # Load slides based on course_id instead of hardcoded path 12 | slides_data = s3_utils.get_json_from_s3("jasmintechs-tutorion", 13 | s3_utils.get_s3_file_path(username, course_id, "slides.json")) 14 | return slides_data 15 | 16 | 17 | def get_current_slide(assistant_id): 18 | # Get current slide position for this specific user/session 19 | position = redis_client.get(f"slide_position:{assistant_id}") 20 | return int(position) if position else 0 21 | 22 | 23 | def set_current_slide(assistant_id, position): 24 | # Store position in Redis with 24-hour expiry 25 | redis_client.setex(f"slide_position:{assistant_id}", 86400, position) 26 | 27 | 28 | def update_viewing_slide(assistant_id, position): 29 | """Store the slide position the user is currently viewing""" 30 | print(f"Updating viewing slide position for {assistant_id} to {position}") 31 | redis_client.setex(f"viewing_slide_position:{assistant_id}", 86400, position) 32 | 33 | def go_to_next_slide(assistant_id, course_id, username): 34 | print(f"Going to next slide for {assistant_id}") 35 | slides = get_slides(course_id, username) 36 | if not slides: 37 | return "No slides found for this course." 38 | current_position = get_current_slide(assistant_id) 39 | 40 | # Increment position 41 | new_position = current_position + 1 42 | if new_position < len(slides): 43 | set_current_slide(assistant_id, new_position) 44 | # Emit event to frontend via Socket.IO 45 | emit_slide_change(assistant_id, new_position) 46 | return "Here is the transcript you would like to read for the next slide: " + slides[new_position]['transcript'] 47 | else: 48 | return "You're already at the last slide." 49 | 50 | 51 | def go_to_previous_slide(assistant_id, course_id, username): 52 | print(f"Going to previous slide for {assistant_id}") 53 | slides = get_slides(course_id, username) 54 | if not slides: 55 | return "No slides found for this course." 56 | current_position = get_current_slide(assistant_id) 57 | 58 | # Decrement position 59 | new_position = max(0, current_position - 1) 60 | if current_position > 0: 61 | set_current_slide(assistant_id, new_position) 62 | # Emit event to frontend via Socket.IO 63 | emit_slide_change(assistant_id, new_position) 64 | return "Here is the transcript you would like to read for the previous slide: " + slides[new_position]['transcript'] 65 | else: 66 | return "You're already at the first slide." 67 | 68 | 69 | def go_to_specified_slide(assistant_id, course_id, username, slide_number): 70 | slide_number -= 1 # Adjust for 0-based index 71 | print(f"Going to slide {slide_number} for {assistant_id}") 72 | slides = get_slides(course_id, username) 73 | if not slides: 74 | return "No slides found for this course." 75 | if slide_number < 0 or slide_number >= len(slides): 76 | return "Invalid slide number." 77 | set_current_slide(assistant_id, slide_number) 78 | emit_slide_change(assistant_id, slide_number) 79 | return "Here is the transcript you would like to read for the specified slide: " + slides[slide_number]['transcript'] 80 | 81 | 82 | def go_to_viewing_slide(assistant_id, course_id, username): 83 | print(f"Going to viewing slide for {assistant_id}") 84 | slides = get_slides(course_id, username) 85 | if not slides: 86 | return "No slides found for this course." 87 | 88 | # Get the viewing slide position 89 | viewing_position = redis_client.get(f"viewing_slide_position:{assistant_id}") 90 | viewing_position = int(viewing_position) if viewing_position else 0 91 | 92 | # Update the assistant's current slide position 93 | set_current_slide(assistant_id, viewing_position) 94 | 95 | # Emit event to frontend via Socket.IO 96 | emit_slide_change(assistant_id, viewing_position) 97 | 98 | return "Here is the transcript for the slide you're currently viewing: " + slides[viewing_position]['transcript'] 99 | 100 | 101 | def go_to_starting_slide(assistant_id, course_id, username): 102 | print(f"Going to starting slide for {assistant_id}") 103 | slides = get_slides(course_id, username) 104 | if not slides: 105 | return "No slides found for this course." 106 | 107 | # Get the last saved position from S3 108 | starting_position = s3_utils.get_assistant_last_position(username, course_id) 109 | 110 | # Ensure position is valid for this course 111 | if starting_position >= len(slides): 112 | starting_position = 0 # Default to first slide if position is invalid 113 | 114 | # Save the current position in Redis 115 | set_current_slide(assistant_id, starting_position) 116 | 117 | # Emit event to frontend via Socket.IO 118 | emit_slide_change(assistant_id, starting_position) 119 | 120 | # Return customized message based on whether we're starting from beginning or resuming 121 | if starting_position == 0: 122 | return f"Welcome to this course. Let's start with the first slide: {slides[starting_position]['transcript']}" 123 | else: 124 | return f"Welcome back to the course. Let's continue from where you left off. Slide {starting_position + 1}: {slides[starting_position]['transcript']}" 125 | 126 | -------------------------------------------------------------------------------- /backend/utils/user_utils.py: -------------------------------------------------------------------------------- 1 | from base64 import b64decode 2 | import os 3 | import firebase_admin 4 | from firebase_admin import credentials, auth 5 | import logging 6 | from typing import Optional, Tuple, Dict, Any 7 | from .firebase_admin import verify_firebase_token 8 | from flask import request, jsonify, make_response 9 | import utils.s3_utils as s3_utils 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | def get_current_user(request): 14 | """Get the current user's email from the request.""" 15 | try: 16 | # For OPTIONS requests, return None to allow CORS preflight 17 | if request.method == 'OPTIONS': 18 | return None 19 | 20 | # Try to get email from cookies first 21 | user_email = request.cookies.get('user_email') 22 | if user_email: 23 | return user_email 24 | 25 | # If not in cookies, try to get from Authorization header 26 | auth_header = request.headers.get('Authorization') 27 | if not auth_header: 28 | logger.warning("No authorization header provided") 29 | return None 30 | 31 | # Remove 'Bearer ' prefix if present 32 | if auth_header.startswith('Bearer '): 33 | token = auth_header[7:] 34 | else: 35 | token = auth_header 36 | 37 | # Verify the token and get the email 38 | decoded_token = verify_firebase_token(token) 39 | if not decoded_token: 40 | logger.warning("Invalid token provided") 41 | return None 42 | 43 | user_email = decoded_token.get('email') 44 | if not user_email: 45 | logger.warning("No email found in token") 46 | return None 47 | 48 | return user_email 49 | except Exception as e: 50 | logger.error(f"Error getting current user: {str(e)}") 51 | return None 52 | 53 | def get_user_folder(upload_folder, username): 54 | return os.path.join(upload_folder, username) 55 | 56 | def handle_verify_user(): 57 | """ 58 | Handle the verify-user route request 59 | 60 | Returns: 61 | Flask response object 62 | """ 63 | if request.method == 'OPTIONS': 64 | response = make_response() 65 | response.headers.add('Access-Control-Allow-Origin', request.headers.get('Origin', 'http://localhost:3000')) 66 | response.headers.add('Access-Control-Allow-Methods', 'POST, OPTIONS') 67 | response.headers.add('Access-Control-Allow-Headers', 'Content-Type, Authorization') 68 | response.headers.add('Access-Control-Allow-Credentials', 'true') 69 | return response 70 | 71 | try: 72 | # Get the token from the Authorization header 73 | auth_header = request.headers.get('Authorization') 74 | if not auth_header: 75 | logger.warning("No authorization header provided") 76 | return jsonify({'error': 'No authorization header provided'}), 401 77 | 78 | # Remove 'Bearer ' prefix if present 79 | if auth_header.startswith('Bearer '): 80 | token = auth_header[7:] 81 | else: 82 | token = auth_header 83 | 84 | # Verify Firebase token 85 | decoded_token = verify_firebase_token(token) 86 | if not decoded_token: 87 | logger.warning("Invalid token provided") 88 | return jsonify({'error': 'Invalid token'}), 401 89 | 90 | user_email = decoded_token.get('email') 91 | if not user_email: 92 | logger.warning("No email found in token") 93 | return jsonify({'error': 'No email found in token'}), 401 94 | 95 | # Create user folder in S3 directly using s3_utils 96 | try: 97 | logger.info(f"Creating S3 folder for user {user_email}") 98 | success = s3_utils.check_and_create_user_folder(user_email) 99 | if not success: 100 | logger.error(f"Failed to create S3 folder for user {user_email}") 101 | return jsonify({'error': 'Failed to initialize user storage'}), 500 102 | logger.info(f"Successfully created S3 folder for user {user_email}") 103 | except Exception as e: 104 | logger.error(f"Error creating S3 folder for user {user_email}: {str(e)}") 105 | return jsonify({'error': f'Error creating user storage: {str(e)}'}), 500 106 | 107 | # Prepare the response data 108 | response_data = { 109 | 'message': 'User verified successfully', 110 | 'email': user_email, 111 | 'courses': [] # Return empty list since we're not fetching courses 112 | } 113 | 114 | # Create response object 115 | response = make_response(jsonify(response_data)) 116 | 117 | # Set CORS headers 118 | response.headers.add('Access-Control-Allow-Origin', request.headers.get('Origin', 'http://localhost:3000')) 119 | response.headers.add('Access-Control-Allow-Methods', 'POST, OPTIONS') 120 | response.headers.add('Access-Control-Allow-Headers', 'Content-Type, Authorization') 121 | response.headers.add('Access-Control-Allow-Credentials', 'true') 122 | 123 | # Set the email cookie 124 | response.set_cookie( 125 | 'user_email', 126 | user_email, 127 | httponly=True, 128 | samesite='Lax', 129 | path='/', 130 | max_age=86400 # 24 hours 131 | ) 132 | 133 | logger.info(f"Returning response with cookie for {user_email}") 134 | return response 135 | 136 | except Exception as e: 137 | logger.error(f"Error in verify_user: {str(e)}") 138 | return jsonify({'error': str(e)}), 500 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /backend/utils/load_and_process_index.py: -------------------------------------------------------------------------------- 1 | import tiktoken 2 | import openai 3 | # import faiss # Comment out the direct import 4 | import numpy as np 5 | from difflib import SequenceMatcher 6 | import time 7 | import io 8 | import json 9 | import boto3 10 | from utils.s3_utils import ( 11 | get_course_s3_folder, 12 | upload_json_to_s3, 13 | upload_faiss_index_to_s3, 14 | ACCESS_KEY, 15 | SECRET_KEY, 16 | REGION_NAME 17 | ) 18 | 19 | # Try to import faiss, make it optional 20 | try: 21 | import faiss 22 | FAISS_AVAILABLE = True 23 | except ImportError: 24 | print("Warning: FAISS not available in load_and_process_index. Vector index functionality will be disabled.") 25 | FAISS_AVAILABLE = False 26 | faiss = None 27 | 28 | 29 | def process_course_context_s3(bucket_name, username, coursename, api_key, max_tokens=2000): 30 | """Standalone function to process course files from S3 and upload indices back to S3""" 31 | start_time = time.time() 32 | 33 | # Initialize S3 client 34 | s3 = boto3.client('s3', 35 | aws_access_key_id=ACCESS_KEY, 36 | aws_secret_access_key=SECRET_KEY, 37 | region_name=REGION_NAME) 38 | 39 | # 1. Load and combine text files from S3 40 | course_prefix = get_course_s3_folder(username, coursename) 41 | all_text = [] 42 | 43 | try: 44 | # List and read text files 45 | response = s3.list_objects_v2(Bucket=bucket_name, Prefix=course_prefix) 46 | for obj in response.get('Contents', []): 47 | if obj['Key'].endswith('.txt'): 48 | file_obj = s3.get_object(Bucket=bucket_name, Key=obj['Key']) 49 | all_text.append(file_obj['Body'].read().decode('utf-8')) 50 | 51 | if not all_text: 52 | raise ValueError("No text files found in course directory") 53 | 54 | combined_text = '\n'.join(all_text) 55 | del all_text # Free memory early 56 | 57 | except Exception as e: 58 | print(f"Error loading files from S3: {str(e)}") 59 | return False 60 | 61 | # 2. Split into chunks with memory efficiency 62 | encoder = tiktoken.encoding_for_model("gpt-4") 63 | chunks = [] 64 | current_chunk = [] 65 | current_token_count = 0 66 | 67 | for line in combined_text.split('\n'): 68 | line_tokens = len(encoder.encode(line + '\n')) 69 | if current_token_count + line_tokens > max_tokens: 70 | if current_chunk: 71 | chunks.append('\n'.join(current_chunk)) 72 | current_chunk = [] 73 | current_token_count = 0 74 | # Handle long lines that exceed max_tokens 75 | while line_tokens > max_tokens: 76 | chunks.append(line[:len(line) // 2]) 77 | line = line[len(line) // 2:] 78 | line_tokens = len(encoder.encode(line + '\n')) 79 | current_chunk.append(line) 80 | current_token_count = line_tokens 81 | else: 82 | current_chunk.append(line) 83 | current_token_count += line_tokens 84 | 85 | if current_chunk: 86 | chunks.append('\n'.join(current_chunk)) 87 | del combined_text # Free memory 88 | 89 | # 3. Generate embeddings and build FAISS index (only if FAISS is available) 90 | faiss_index = None 91 | if FAISS_AVAILABLE: 92 | dimension = 3072 # text-embedding-3-large dimension 93 | faiss_index = faiss.IndexFlatL2(dimension) 94 | embeddings = [] 95 | 96 | openai_client = openai.OpenAI(api_key=api_key) 97 | 98 | # Process chunks in batches to control memory usage 99 | batch_size = 100 100 | for i in range(0, len(chunks), batch_size): 101 | batch = chunks[i:i + batch_size] 102 | try: 103 | response = openai_client.embeddings.create( 104 | model="text-embedding-3-large", 105 | input=batch 106 | ) 107 | batch_embeddings = [e.embedding for e in response.data] 108 | embeddings.extend(batch_embeddings) 109 | except Exception as e: 110 | print(f"Error generating embeddings: {str(e)}") 111 | embeddings.extend([np.zeros(dimension).tolist()] * len(batch)) 112 | 113 | # Clear memory between batches 114 | del batch 115 | del response 116 | 117 | # Convert to numpy array and add to FAISS 118 | embeddings_np = np.array(embeddings).astype('float32') 119 | faiss_index.add(embeddings_np) 120 | del embeddings 121 | del embeddings_np 122 | else: 123 | print("Warning: FAISS not available. Skipping vector index creation.") 124 | 125 | # 4. Build inverted index 126 | inverted_index = {} 127 | for i, chunk in enumerate(chunks): 128 | quotes = [line for line in chunk.split('\n') if line.startswith('"')] 129 | for quote in quotes: 130 | inverted_index[quote.lower()] = i 131 | 132 | # 5. Upload all artifacts to S3 133 | base_key = get_course_s3_folder(username, coursename) 134 | 135 | # Upload chunks 136 | upload_json_to_s3(chunks, bucket_name, f"{base_key}chunks.json") 137 | del chunks 138 | 139 | # Upload FAISS index (only if available) 140 | if FAISS_AVAILABLE and faiss_index is not None: 141 | upload_faiss_index_to_s3(faiss_index, bucket_name, f"{base_key}faiss.index") 142 | del faiss_index 143 | else: 144 | print("Warning: FAISS index not created or FAISS not available. Skipping FAISS index upload.") 145 | 146 | # Upload inverted index 147 | upload_json_to_s3(inverted_index, bucket_name, f"{base_key}inverted_index.json") 148 | del inverted_index 149 | 150 | print(f"Total processing time: {time.time() - start_time:.2f} seconds") 151 | return True 152 | -------------------------------------------------------------------------------- /app/ai-tutor/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState, useRef, useEffect } from "react" 4 | import { MainNav } from "@/components/main-nav" 5 | import { Button } from "@/components/ui/button" 6 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" 7 | import { Input } from "@/components/ui/input" 8 | import { Textarea } from "@/components/ui/textarea" 9 | import { Bot, Send } from 'lucide-react' 10 | import { ScrollArea } from "@/components/ui/scroll-area" 11 | 12 | interface Message { 13 | id: number 14 | sender: "user" | "ai" 15 | text: string 16 | timestamp: Date 17 | } 18 | 19 | export default function AITutorPage() { 20 | const [messages, setMessages] = useState([ 21 | { 22 | id: 1, 23 | sender: "ai", 24 | text: "Hello! I'm your AI tutor. How can I assist you today?", 25 | timestamp: new Date() 26 | } 27 | ]); 28 | const [inputMessage, setInputMessage] = useState(""); 29 | const [isTyping, setIsTyping] = useState(false); 30 | 31 | const handleSendMessage = async () => { 32 | if (!inputMessage.trim()) return; 33 | 34 | // Add user message to chat 35 | const userMessage: Message = { 36 | id: Date.now(), 37 | sender: "user", 38 | text: inputMessage.trim(), 39 | timestamp: new Date() 40 | }; 41 | setMessages(prev => [...prev, userMessage]); 42 | setInputMessage(""); // Clear input 43 | setIsTyping(true); 44 | 45 | try { 46 | // Send message to AI tutor endpoint 47 | const response = await fetch("http://127.0.0.1:5000/api/aitutor-response", { 48 | method: "POST", 49 | headers: { "Content-Type": "application/json" }, 50 | body: JSON.stringify({ input: userMessage.text }), 51 | }); 52 | 53 | if (!response.ok) { 54 | throw new Error('Network response was not ok'); 55 | } 56 | 57 | const data = await response.text(); // Changed from response.json() since your endpoint returns text directly 58 | 59 | // Add AI response to chat 60 | const aiMessage: Message = { 61 | id: Date.now(), 62 | sender: "ai", 63 | text: data, 64 | timestamp: new Date() 65 | }; 66 | setMessages(prev => [...prev, aiMessage]); 67 | } catch (error) { 68 | console.error("Error:", error); 69 | // Add error message to chat 70 | const errorMessage: Message = { 71 | id: Date.now(), 72 | sender: "ai", 73 | text: "Sorry, I encountered an error. Please try again.", 74 | timestamp: new Date() 75 | }; 76 | setMessages(prev => [...prev, errorMessage]); 77 | } finally { 78 | setIsTyping(false); 79 | } 80 | }; 81 | 82 | return ( 83 |
84 | 85 |
86 |

AI Tutor

87 |
88 | 89 | 90 | Chat with AI Tutor 91 | 92 | 93 | 94 |
95 | {messages.map((message) => ( 96 |
101 | {message.sender === "ai" && ( 102 | 103 | )} 104 |
108 |

{message.text}

109 | 110 | {message.timestamp.toLocaleTimeString()} 111 | 112 |
113 |
114 | ))} 115 | {isTyping && ( 116 |
117 | 118 |
119 |

Typing...

120 |
121 |
122 | )} 123 |
124 |
125 |
126 | setInputMessage(e.target.value)} 130 | onKeyDown={(e) => { 131 | if (e.key === 'Enter' && !e.shiftKey) { 132 | e.preventDefault(); 133 | handleSendMessage(); 134 | } 135 | }} 136 | /> 137 | 145 |
146 |
147 |
148 | 149 | 150 | Study Notes 151 | 152 | 153 |